Compare commits

...

1442 Commits

Author SHA1 Message Date
b711cd258a Release 2.0.3 2019-06-12 18:47:11 +01:00
e1c4069a72 [Gtk] Refactor presenting window
Include the correct usage for other display servers.

Still not sure how to get the proper timestamp for Wayland or Quartz but
I read that using 0 equals the GDK_CURRENT_TIME which suffices for now.
2019-06-12 16:57:48 +01:00
a2dee79439 [GTK] Improve detecting X11 display server
GdkX11 still imports on Wayland so check display server is X11 before
importing.
2019-06-12 16:05:15 +01:00
7a54db3179 [Docs] Fix typo and url for Windows install 2019-06-12 14:56:21 +01:00
03e7952d26 [GTK] Only import wnck on X11 display
Wnck is only supported on X11 and raises errors in Wayland so only load
it when X11 present.

Fixes: #3265
2019-06-12 10:21:23 +01:00
7ee8750be4 [GTK] Fix peers tab flag tooltip error
Hovering over a country flag resulted in an AttributeError.

This is due to get_tooltip_context now returning a bool value instead of
the tooltip object.

Fixes: #3219
2019-06-12 09:40:51 +01:00
f61001a15d [GTK] Fix missing argument for GtkMenu.popup()
Missed while converting from pygtk to Gtk3

Fixes: #3266
2019-06-12 09:40:14 +01:00
86ddadacf7 [Extractor] Fix startup error
On Python 3 need to create a copy of the dict to iterate

Fixes: #3264
2019-06-12 09:40:14 +01:00
632089940c [Web] Fix unable to change password
The hashlib update method requires bytes and raised a TypeError for salt
passed as text.

Added a test for auth change_password

Fixes: #3262
2019-06-11 20:14:11 +01:00
5d7db3e727 [Web] Change request.base path encoding to utf-8
A user reported a problem with setting base path resulting in this error:

    encoding with 'idna' codec failed (UnicodeError: label too long)

It is likely the base path is longer than 63 chars, which is unusual,
however the idna codec is for domain name not paths so switch to utf-8.

Fixes: #3261
2019-06-11 20:14:11 +01:00
4dd1f63b8b [Web] Fix TypeError with reverse proxy x-deluge-base header
The request header needs decoded otherwise string comparisons fail.

Fixes: #3260
2019-06-11 20:14:11 +01:00
fc134cdffb [Docs] Add more info to release notes 2019-06-11 20:14:11 +01:00
36cb4c5a4f [Docs] Updates to release checklist 2019-06-11 20:14:11 +01:00
676bdb26e0 [Docs] Remove incomplete Windows install instructions
The instructions are in-progress and missing steps so instead point to
the issue ticket for now.
2019-06-11 12:35:04 +01:00
dff778ceeb [i18n] Try loading intl.dll on Windows 2019-06-11 12:35:04 +01:00
bdadd2b515 Fix typo in install instructions for macOS 2019-06-11 12:35:04 +01:00
a34543100c [Web] Fix peers tab failing to set flag location
The request.country returns bytes not a string so decode.
2019-06-11 12:35:04 +01:00
b8b044f451 [lint] Fix pre-commit config key name 2019-06-10 14:24:47 +01:00
2d87cde887 Make a 2.0.2 release 2019-06-08 21:34:27 +01:00
212efc4f52 [Packaging] Move user out of systemd files and add to tarball
With the `deluge` user specified in the unit files it ties it to
that user and makes it unavailable for re-use by systemd user instance.

Remove the user and group from the unit files and put them in a separate
`user.conf` file that should be installed as an override file e.g. for
deluged.service this would be placed as follows:

    /etc/systemd/service/deluge.service.d/user.conf

Add the systemd files to the tarball for package maintainers.

Closes: #2034
2019-06-08 21:31:49 +01:00
879a397215 [Packaging] Add updated launchd scripts
Copy from Trac UserGuide and updated with proper naming and deluge-web
version.

The bin location is default for brew with pip install.

Closes: #3073
2019-06-08 21:31:49 +01:00
957cd5dd9c [Core] Fix SimpleNamespace on Python2 2019-06-08 21:31:49 +01:00
25087d3f2d [Docs] Add release notes and update pages 2019-06-08 16:42:25 +01:00
d24109f0a2 Update Changelog for 2.0.1 2019-06-07 20:27:02 +01:00
647baebcf0 [Docs] Update release checklist 2019-06-07 14:47:49 +01:00
98ce3cd385 [Packaging] Create tar.gz with sdist for PyPi
PyPi does not accept `tar.xz` source tarballs!
2019-06-07 14:47:02 +01:00
0c87d9bd7d [Packaging] Fix get_version with no git command
An unhandled FileNotFoundError was encounted if git command was not available.
2019-06-07 14:45:49 +01:00
aa35247e95 Back to dev 2019-06-06 17:27:49 +01:00
0dc4e18ac4 Updates for 2.0.0 release 2019-06-06 17:12:58 +01:00
d4185505d1 [Docs] Cleanup changelog and docs 2019-06-06 17:12:58 +01:00
04e58659fe Update translation files 2019-06-06 16:50:16 +01:00
5e738cf73a Simplify the get_version method
Use post segment instead of dev for non-dev tags.
Default to 'deluge-' and '.dev0' to simplify getting version.
Refactor to use subprocess.check_output
Use deluge.common.get_version as fallback in docs conf.
2019-06-06 11:30:45 +01:00
ce8595e8dd [Tests] Remove python2 from tox config 2019-06-06 11:30:45 +01:00
be74d96c6a [Core] Copy lt alerts to avoid segfaults
Changes in libtorrent 1.1 mean that alerts are no longer allowed to be
accessed after the next call to pop_alerts.

> It is safe to call pop_alerts from multiple different threads, as
long as the alerts themselves are not accessed once another thread
calls pop_alerts. Doing this requires manual synchronization between
the popping threads.

The solution is to copy the alert attributes and pass that to the
handlers.

Refs: https://github.com/arvidn/libtorrent/issues/2779
      #3159
2019-06-05 15:10:35 +01:00
4212bd6800 [Travis] Add unit test for libtorrent 1.2
Skip test_torrent_error_resume_original_state on libtorrent 1.2 as it
is failing for an unknown reason.

Refs: #3255
2019-06-05 15:09:40 +01:00
d40d40af31 [Core] Update to support libtorrent 1.2
Some changes between lt 1.1 and 1.2 require updates to core code.
 - Switch from proxy_type to proxy_type_t
 - Replace hardcoded flag value with add_torrent_params_flags_t since
   1.2 uses different flag values.
 - add_torrent_params requires flags set instead of dict values.

Refs: #3255
2019-06-05 15:09:00 +01:00
cbf9ee8978 fix blurry icons in gnome
Fixed blurry icon by changing "StartupWMClass" value from "Deluge" to "deluge" in "deluge.desktop".
2019-06-04 17:39:06 +01:00
7abeb4ee0f [packaging] sdist use xztar and exclude .mo files 2019-05-24 11:21:35 +01:00
bd4a3cba38 [Docs] Update install details and add more pages 2019-05-23 15:41:58 +01:00
3cfa39a2ad [macOS] Fix GTK windowing issues
On macOS the Quartz windowing is used instead of X11 so make ensure
that the X11 window calls are optional.

Also if gtkosx_application is not available then don't create osxapp.

It would be useful to find out how to pass window timestamps on Quartz.
2019-05-23 13:00:02 +01:00
a3b6d8d8e5 [Appveyor] Switch to Python 3.6
- Change build from Python 2.7 to 3.6 64-bit.
 - Specify py36 for tox since using py3 will choose latest py3 e.g. 3.7!
 - Use python 3.6 libtorrent.pyd build
 - Use pre-installed OpenSSL 1.1 (matches libtorrent build)
 - Add python version output to tox.ini for debugging.
2019-05-23 00:15:49 +01:00
7e3692bb5a [Docs] Fix missing path in RTD config 2019-05-22 18:01:05 +01:00
aa3a9a15cc [Docs] Update readthedocs config
- Add the missing package install so pkg_resources can find version.
2019-05-22 17:48:33 +01:00
0f92ea401f [Travis] Remove Python2 tests 2019-05-22 14:14:44 +01:00
260d55aeae [Travis] Fix getting version from git tags
Travis clones the git repo with a shallow depth and the git describe
command cannot find the version tag. Setting depth to 1000 should be
enough but can be increased if required.

Refs:
  https://docs.travis-ci.com/user/customizing-the-build/#git-clone-depth
  https://stackoverflow.com/a/51727114/175584
2019-05-22 12:12:54 +01:00
a9609a197d [Docs] Fix ReadTheDocs config path 2019-05-22 11:39:37 +01:00
a8fac1381b [Packaging] Cleanup README for Pypi
- Set a minimal Python version 3.5 and remove universal wheels.
- Tidy up the README
- Add Project URL for issues and docs.
2019-05-22 11:15:32 +01:00
65f6ede8b2 [Docs] Updates and fixes to build on Python 3
- Updates to the sphinx conf
  - Applied Mock fixes to build on Python 3.
  - Group patches at bottom of conf file.
  - Use just a major.minor for version.
  - Specify Sphinx 2.0 version requirement.
- Move requirements.txt to docs dir.
- Add readthedocs config
- Fix docstring code block rst formatting issue.
2019-05-21 15:23:45 +01:00
515dbcc5d9 Minor updates to log.py 2019-05-20 21:23:20 +01:00
827987fe7d [GTK] Fix drag and drop files in files_tab
Encoutered an error reordering files by dragging in the files tab:
   TypeError: can't pickle TreePath objects

The issue was get_selected_row  now returns a list of TreePath objects which
cannot be pickled. Also the set_text method only accept unicode text to
pickled bytes cannot be used.

The fix is to convert the TreePaths to strings and use json to encode
the list of strings for set_text.
2019-05-20 21:14:42 +01:00
1357ca7582 [i18n] Ignore non-translation dirs in get_languages
The `__pycache__` dir was showing up in list of available languages so
ensure only those directories with languages are returned.
2019-05-20 21:02:13 +01:00
72d363968e [Logging] Fix line numbers missing on Python 3
The findCaller method returns a 4-element tuple on Python 3 whereas it
was a 3-element tuple on Python 2.
2019-05-20 16:49:49 +01:00
c6b6902e9f [Core] Fix prefetch magnets missing trackers
When adding magnets that have been prefetched the tracker details were
lost. A result of returning only the lt.torrent_info.metadata which
does not contain full torrent details, such as trackers.

- Modified torrentmanager prefetch_metadata to return dict instead of
  base64 encoded bencoded metadata dict...
  - Used a namedtuple to ease identifying tuple contents.
  - Updated tests to reflect changes with mock trackers added to
    test_torrent.file.torrent.

- Refactor TorrentInfo to accept dict instead of bytes and add
  a class method to accept metadata dict with lists of trackers.
  - Rename class arg from metainfo to torrent_file, matching
    lt.torrent_info.
  - Rename metadata property to correct name; metainfo.
  - Simplify class variable naming with _filedata and _metainfo for
    torrent file contents encoded and decoded respectively.

- Update GTK Add torrent dialog to pass trackers to TorrentInfo.
2019-05-20 16:49:25 +01:00
6a5bb44d5b [Core] Write torrent file with full magnet metainfo
The use of torrent_info.metadata misses saves trackers and other torrent
metainfo fields so use lt.create_torrent generate method to create this
data.
2019-05-18 16:15:27 +01:00
cbcf8eb863 [GTK] Fix missing magnet trackers with Add dialog (non-prefetch)
When adding magnets (without prefetch) the trackers were missing.

The issue was that the magnet uri was being xml escaped twice so that
the ampersand was still escaped when passed to core and everything after
the magnet info_hash was ignored.

Also found unneeded call to core.add_torrent_files when only adding
magnets with core.add_torrent_magnet so check torrents_to_add before
calling.
2019-05-18 16:15:27 +01:00
09cfd9b89e [bencode] Fix unhandled TypeError with string
Passing a non-bencoded dict to bdecode resulted in an unhandled
TypeError.

- Catch TypeError in decode_func.
- Add bdecode tests.
- Replace KeyError and IndexError with base LookupError.
2019-05-18 16:15:27 +01:00
b961e11df6 [GTK] Fix handling failed magnet prefetching in Add dialog
If a torrent already exists when trying to prefetch magnet metadata the
exception is not handled and dialog does not update correctly.
2019-05-18 16:12:08 +01:00
2ca683e8fe [Daemon] Fix showing translation warning messages
Disable the warn_msg in daemon_entry since the argparse option are all
marked for translation and warnings are being output. The original
intent of this warn_msg was to identify anything in core code that was
incorrectly marked for translation.
2019-05-17 10:32:07 +01:00
fd20addead Raise Twisted minimum version to 17.1
The use of CertificateOptions with raiseMinimumTo requires this new
minimum version so update requirements and documents.
2019-05-17 10:08:48 +01:00
535b13b5f1 [Plugins] Convert plugins to deluge_ module prefix convention
This commit reverts namespace for the plugins and uses a module prefix
"deluge_" in it's place. The distribution package name remains the same
for now but will also be considered to use a prefix to help find the
third-party plugins e.g. Deluge-{Plugin} and the pluginmanager will
strip the prefix for displaying.

The change is a result of problems trying to package Deluge with
pyinstaller and the pkg_resources namespaces is not compatible.
Testing alternatives to using the pkgutil or PEP420 (native) namespaces
did not yield any joy either as importing eggs with namespaces does not
work. [1]

At this point importable eggs are considered deprecated but there is no
viable alternative yet. [2]

[1] https://github.com/pypa/packaging-problems/issues/212
[2] https://github.com/pypa/packaging-problems/issues/244
2019-05-15 19:20:08 +01:00
d6a0276a78 Update gitignore file
- pip install creates dist-info directories
- add __pycache__ for compelteness and other byte compiled file types.
2019-05-14 11:48:58 +01:00
9c0325b129 [Plugins] Remove stray unneeded init files 2019-05-13 18:55:35 +01:00
f885edd7fc [Console] Add move completed option to add torrent command
- Added a -m|--move-completed option for specifying a move completed
  path when adding a torrent.
- Re-used existing console test and renamed for generic usage.
- Moved setup_translation to tests.__init__ so it is always setup
  instead of relying on tests.common import.

Closes #2847

Co-authored-by: Calum Lind <calumlind+deluge@gmail.com>
2019-05-11 22:33:18 +01:00
2b171e58a3 [Web] Fix missing deregister_object
pluginbase has the complementary deregister_object but the actual
method was missing in JSON component.
2019-05-11 21:04:34 +01:00
d417c4b0f9 [Core] Refactor the base argparser and translation code.
- Move baseargparser out of deluge/ui since it is also used by the
  Daemon and could cause packaging issues if UI code is not available.
  - Renamed baseargparser to argparserbase to follow existing Deluge
    naming.
  - Renamed get_version to distinguish from deluge.common.get_version.
- Translation code is usable by more than just the UIs so also move it
  to Deluge namespace and re-use i18n directory and make it a package.
  - Renamed setup_translations to singular as it felt more correct.
  - Renamed set_dummy_trans to be more descriptive.

Closes: #3081
2019-05-11 20:40:20 +01:00
653f80eac8 [#3250|Console] Fix errors dispaying magnets with no files
Trying to display the files for magnet that has no metadata and thus no
files resulted in `'NoneType' object is not iterable` errors.

Only call file prio update method if we have a list of files.
2019-05-11 19:09:44 +01:00
76b89a7943 [#3250|Console] Fix unable to remove torrent
Trying to remove a torrent had no effect and resulted in the following
error: `'TorrentList' object has no attribute 'clear_marked'`

 * The clear_marked call needs to be made with torrentview instead.
 * Ensure the popup dialog is closed upon deleting the torrent.
2019-05-11 19:05:42 +01:00
6ff7a5400f Remove detox development requirement
As of tox 3.7, detox is no longer needed as tox added a native
parallel environment execution.
2019-05-09 12:02:59 +01:00
db021b9f41 [#3244|Web] Add support for accept-encoding header
* Use EncodingResourceWrapper to replace compress function so that the
proper checks for accept-encoding header are made.
* Ensure only text is compressed and images are left uncompressed.
2019-05-09 11:41:00 +01:00
ab4661f6fd [Packaging] Remove distro from setuptool requirements
The distro package is only available for Ubuntu 18.04 onwards so don't
require it as it will runtime error about missing module.
2019-05-09 09:20:29 +01:00
396cadefda [Tests] Fix console test fail on AppVeyor
Curses is not available on Windows so skip AppVeyor test that errors
with:

    E       NameError: global name 'curses' is not defined
2019-05-08 21:41:28 +01:00
2296906ed3 [Common] Replace platform.linux_distribution function
As of python 3.5, this function is marked as deprecated.
So, [distro][1] is the one we will use (this package is listed at the
example package in the python's [docs][2]).

[1] https://pypi.org/project/distro/
[2] https://docs.python.org/3/library/platform.html#platform.dist
2019-05-08 21:24:45 +01:00
1a134cab1b [#3248|Console] Fix not accepting input under Python3
On Python 3 the chr function returns unicode so trying to decode will
result in an error. Applied a workaround to assign without decoding.
2019-05-08 21:02:09 +01:00
7d67792493 [Lint] Update pre-commit linter versions
- Default to python3.
- Needed to add six to isort config.
2019-05-03 17:50:42 +01:00
3c18e890e8 [Tests] Fix AttributeError in test_core with Twisted 19
Should be setting header contents to string not ints and latest version
of Twisted raised an error encountering int. Also correct the header
name for setting length.
2019-05-03 15:35:32 +01:00
615500e6e6 [Plugins] Fix missing deregister for JSON 2019-05-03 14:57:30 +01:00
1425fe5413 [Tox] Pin pip version to fix PEP517 issues
* Using pyproject.toml for black config pip version 19.1 errors out
   about using editable install with pyproject.toml.
   Workaround is to not use pip 19.1 in tox.
 * Pin to 18.1 to avoid pip-wheel-metadata-folder creation

Ref:
 - https://github.com/pypa/pip/issues/6434
 - https://github.com/pypa/pip/issues/6213
2019-05-03 14:53:34 +01:00
84643fb6f7 [GTK] Remove running reactor in Gdk thread
The Gdk threading code is causing issue on Windows and this method
of moving the main loop to a thread has been deprecated and advised
against so removing without adding replacement as it currently
only creates one main thread and should still be fine.

If a blocking operation occures and needs solving, see the pygobject
guide for recommened way to use threads in Gtk:

https://pygobject.readthedocs.io/en/latest/guide/threading.html
2019-03-29 15:13:02 +00:00
c8b621172e [Lint] Fix flake8 3.7 warnings
- Fix new flake8 warnings from latest version.
  Note: The `addSlash` variable was orphaned with no reference in
        Twisted or Deluge code so removed.

- Update pre-commit config
  - New pinned versions.
  - Fix prettier output.
  - Use new flake8 hook config and add naming plugin.
2019-03-29 14:27:30 +00:00
02e07dda2a [Docs] Fix recommonmark monkey patch and pin version
The refactored patch did not work so revert it.

A new release of recommonmark breaks the docs build so pin it to working version.
2019-03-29 14:27:18 +00:00
b2e19561e6 [GTK] Fix file manager window popup behind Deluge
Added 'TIMESTAMP' key to startup-id string for dbus method. Unsure if
this is the correct way to specify startup id but it seems to work.

Recreate the dbus session with each call since if there is an error
with the dbus method then it will crash and subsequent calls will fail
with a cryptic message:

   dbus error the name was not provided by any .service files
2018-11-17 14:36:38 +00:00
389f4167b2 [i18n] Fix incorrect GB translation 2018-11-17 12:17:30 +00:00
63cc745f5b [Core|Py3] Fix fastresume data not being loaded
Decode the resume_data dict keys to fix lookups with torrent_id strings
not finding resume_data.
2018-11-17 12:12:55 +00:00
1a4ac93fbb [Lint] Bump prettier version to 1.15.2 2018-11-16 15:06:30 +00:00
582f60ea0b [Tests] Fix failing tracker_icons test
Disable the testing with seo.com as the site certificate has expired.

Ideally should not be testing against live sites and instead use request
replay tool such as VCR.py.
2018-11-16 15:06:30 +00:00
157f6ff62a [WebUI] Catch unhandled 'Bad host id' exception
The bad host id error usually occurs on webui when the 'default_daemon'
key in web.conf does not exist in hostlist.conf.

Added a errback method to output a more useful log message.
2018-11-16 15:06:30 +00:00
bf4244e8b2 [GTK] Fix adding non-ascii torrents and paths on Py2
Added decode_bytes to all widgets returning text to ensure unicode on
Python 2.
2018-11-16 15:06:30 +00:00
25cfd58792 [GTK] Refactor deluge.common usage 2018-11-16 15:06:30 +00:00
09d04aaac0 [Core] Fix showing incorrect file priorities
Removed previous workaround to ensure sync of file priorities with
libtorrent. This did not work when loading torrents as the status is
called before setting the file priorites and resets them to default.

Removed the call to set_file_priorities when writing the torrent file
to disk as it resets the options to default so although the torrent
file priorities do not change, the priorities for UIs is incorrect.
2018-11-16 15:06:30 +00:00
27b4e2d891 [Docs] Fix formatting of exported docstrings 2018-11-16 15:06:30 +00:00
043344b986 Modify the transfer protocol in a couple ways.
Replace the 'D' header with an unsigned byte that indicates the protocol version. This will allow easier changes to protocol in the future.
Replace the signed integer used for message length with an unsigned 32-bit integer. There is no need for a signed value here as a message length must always be positive. This also doubles the max message length.
2018-11-12 19:44:00 -08:00
3b8f71613b [Packaging] Fix deps for win32
- Fixed trying to install py2-ipaddress breaking on Python3.
- Add wheel universal option so Py2 and Py3 wheel built.
2018-11-12 10:10:41 +00:00
10fcbecc04 [Common] Fix win32 set env issue on Python 3
- On Python 3 find_msvcrt returns None and _wputenv should be used with
unicode strings.
- Removed the alternative msvcrt set env since `cdll.msvcrt` should suffice.
- Removed the broad exception catching.
2018-11-12 10:05:45 +00:00
ab7f19fbb8 [GTK] Fix no torrent selected on startup 2018-11-09 10:41:08 +00:00
b665a4a6f7 [#3211|Packaging] Fix missing tray icon
Fixed not including deluge-panel.png in the packaging install.
2018-11-09 10:40:03 +00:00
2c45e59900 Fix UnicodeDecodeErrors with files containing non-ascii chars
The main issue here is a user trying to start deluge and the XDG
`user-dirs.dirs` file contains dir names with non-ascii chars causing
a UnicodeDecodeError when comparing with unicode chars since Py2
default encoding is ascii.

The solution is to use io.open as used elsewhere in code with
encoding set to utf8. Applied to all usage of open in common.
2018-11-08 22:37:26 +00:00
89868cc944 [GTK] Fix needing bytes with hashlib on Py3 2018-11-07 15:52:26 +00:00
841cb889aa [Execute] Fix Glade layout and Py3 bytes issue 2018-11-07 15:52:26 +00:00
6b2f14e51e [GTK] Fix windows not showing topmost on desktop
When showing the main_window, Add dialog or file manager windows they
would not appear at the top of the display stack, always one below.

This is due to needing the windowing timestamp to be passed when making
these calls. The recommended Gtk solution to use present_with_time and
use an event.time timestamp. However, this does not always work so
instead used the lower level Gdk set_user_time and fetch timestamp from
X11 server.

Notes:
- Using int(time.time()) for timestamp is not correct as the
  windowing timestamp is different.
- Gtk.get_current_event_time only works when there is an event being
  processed.
- It might be useful for non-X11 windowing systems to store event
  timestamps so that we have a value to use instead of 0.
2018-11-07 15:52:26 +00:00
7e2192e875 [GTK] Fix showing sidebar and tabsbar
- Fixed the sidebar position not being restored by applying the config
  value in main_window first_show and updating config in position
  callback.
- Renamed the main_window vpaned and hpaned widgets to aid identifying
  purpose.
- Fixed filtertreeview KeyError when not conneted and hiding tabsbar.
- Fixed the tabsbar notebook not being hidden on restart by adding a
  new config value.
2018-11-07 15:52:26 +00:00
f11a42b9bf [GTK] Remove old builtin notification config values 2018-11-06 18:15:23 +00:00
845204178b [AutoAdd] Fix GTK3 AttributeErrors 2018-11-06 14:57:55 +00:00
d937a323fb [GTK] Replace all deprecated VBox and HBox
Use the recommended Gtk.Box.new() signature to match the GTK usage.
2018-11-06 14:57:55 +00:00
d7c48d27d8 [Label] Fix mnemonic labels
Remove the icons to simplify code since ImageMenuItem is deprecated.
2018-11-06 14:57:23 +00:00
1bc766213c Remove stray debug logging lines 2018-11-06 14:57:12 +00:00
775aef5f9b [WebUI] Fix creating icon on GTK3 prefs page 2018-11-06 11:38:21 +00:00
83cac4978a [GTK] Fix and cleanup storing window position 2018-11-06 11:19:00 +00:00
2bb9a8e71c [GTK] Fix tray preferences greyed out
When not connected the tray preferences should still be available.

There is no need to have the `is_connected` applied to widget
sensitivity here as that is set elsewhere in the code.
2018-11-06 11:16:20 +00:00
39783c7703 [GTK] Fix systray popup TypeError
An incorrect number of arguments supplied to GtkMenu.popup
2018-11-06 10:47:17 +00:00
9f9f564e62 [GTK] Fix unicode warnings on Python 2
GTK3 on Python 2 returns bytes so decode before comparisons.
2018-11-05 16:47:58 +00:00
ab1b2bcf14 [Exceute] Fix GTK3 pack_start missing paramters 2018-11-05 16:47:06 +00:00
bb0c61bb3f [GTK3] Replace deprecated set_data with attribute assignment 2018-11-05 08:38:54 +00:00
a7dcf39a32 [GTK3] Fix pathcombo getting folder name on wrong widget 2018-11-05 08:26:23 +00:00
e43796ae51 [GTK] Fix missing sidebar tracker icons
The filename for tracker_icons is an absolute path so check the
path before calling get_pixmap which is for relative ui/data paths.
2018-11-05 08:26:23 +00:00
6655fe67c3 [UI|Core] Fix problems with file priorities
- Fixed the core not correctly settings the current file_priority
settings and added a test.
- Fixed the console not setting file priorities.
- Change the label for not downloading of a file to 'Skip'.
2018-11-05 08:26:23 +00:00
2104b9831c [Core] Fix file_renamed alert returning method
Fixed a typo that resulted in the new_name method being emitted
instead of the string.
2018-11-02 09:00:42 +00:00
e7127637cf [Dependency] Remove bundled rencode 2018-11-02 08:47:57 +00:00
6233e5c844 [Blocklist] Fix detecting compression type on Py3
The magic number is in bytes so ensure bytes in COMPRESSION_TYPES
2018-11-02 08:47:57 +00:00
a01481b26f [Plugins] Update create script and add GTK3 how-to doc
- Updated create_plugin script to create a GTK3 plugin.
- Added a document for updating a 1.3 plugin to be compatible with
  2.0.
2018-11-02 08:47:57 +00:00
3d24998577 [Docs] Fix duplicate description warnings 2018-11-02 08:47:57 +00:00
f24e9d152c [Common] Remove oldest archive if too many exist
Prevents the archive folder bloating.
2018-11-02 08:47:57 +00:00
f47089ae7d [Core] Archive corrupt torrent.state on load
If the torrent.state was corrupted then loading would create a new
state with no backup to examine.

The solution is to use the archive function to save a copy of the
torrent.state.

Added a message argument to archive_files so that the error message
with a reason for archiving can be included in the tarball.
2018-11-02 08:47:57 +00:00
d70abd2986 [Plugins] Refactor plugin scan code
Simplify adding entries to the working_set.
Also fixes adding files rather than just dirs to working_set.
2018-11-02 08:47:57 +00:00
7d998a45f2 Shorten code to declare namespace
This is the recommened way of declaring namespace.

Also remove unneeded unicode_literals import.
2018-11-02 08:47:57 +00:00
3433a911cc [Plugins] Allow enabling any plugin Python version
Users encounter issues when trying to install plugins with differing
python versions. If the plugin was built with Py2.6 but they are using
Py2.7 the plugin would not load. With the move to Python 3 this could
become more of an issue. The workaround is to let the plugin manager
to try to load the deluge plugin regardless of the python version it
was built with.

This will put the onus on plugin author to keep the plugin code
compatible with more Python versions.
2018-11-02 08:47:57 +00:00
967606fa0f [Tests] Fix str/bytes issue on Python 3
- argparser does not accept bytes and raised an error with encoded vars.
2018-11-02 08:47:57 +00:00
97e7d95dd3 Cleanup tox configuration
There were issues with dependencies and tox environments under Python 3
so refactored the tox configuration to be more consistent and clearer.

- Moved travis to default to Python 3 for linting and tests.
- Fixed missing mock for cairo in sphinx config.
- Collated the base deps sections to improve readability.
- Added PYTEST_ADDOPTS env to override pytest verbosity in just tox
  tests as this was a common option being used.
- Renamed env 'testcoverage' to the more concise 'coverage' and moved
  html creation under single env as handy to have this output as well
  as report.
- Cleaned up the isort config for gtk3.
- Added `bad-continuation` to pylint config as conflcts with black
  formatting.
- Fix isort issue with bbfreeze script. This will likely be removed
  in future so just skip sorting it.
2018-11-02 08:47:57 +00:00
26c28445a5 [GTK3] Fix showing piecesbar 2018-11-02 08:45:40 +00:00
74a459274c [GTK3] Cleanup widget placement and spacing 2018-11-02 08:45:40 +00:00
bb6e290bf8 [GTK3] Fix UnicodeWarning in row comparison on Python 2 2018-11-02 08:45:40 +00:00
4a79e1f100 [GTK3] Save ui files with Glade 3.22
To ensure properties are updated this is a simple open and save with the
glade designer. Always a bit messy with the diff but should not
change functionality.
2018-11-02 08:45:40 +00:00
bff93bb162 [Appveyor] Remove PyGTK and disable win32 packaging 2018-11-02 08:45:40 +00:00
bffd091429 Update DEPENDS for GTK3 2018-11-02 08:45:40 +00:00
70d5931622 [GTK3] Fix column header right-click menu popup
The popup_at_pointer method is only available in GTK >=3.22 so for
compatibility restore using popup method.

Right-clicking on column headers popped-up torrent menu so only show
this menu when in torrentview.
2018-11-02 08:45:39 +00:00
ce49cde49d [GTK3] Fix cmp sorting on Python 3 2018-11-02 08:45:39 +00:00
a3bd2e547a [UI] Use Pillow for .ico icons
Pillow added the code in Win32IconImagePlugin to v2.1 (in 2013) so we can remove it.
Refactored the tracker_icons code to reflect this change.
2018-11-02 08:45:39 +00:00
64710ad226 [GTK3] Disconnect after editing host in connection manager 2018-11-02 08:45:39 +00:00
cd6bad0e35 [UI] Fix passing bytes config path to subprocess on Python 3
The % substitution was causing the bytes prefix to become part of the
string and created a `b'/` prefixed config directory. Ensure the config
arg is byte prefixed too.
2018-11-02 08:45:39 +00:00
1310645f55 [GTK3] Translate daemon status in connection manager
- Added a tooltip to show text status which also required translation
  so created a new liststore column for the translated text to ensure
  status is parsed correctly.
- Forced the status to lowercase to avoid translation issues and
  simplify comparisons.
2018-11-02 08:45:39 +00:00
e6a7119595 [GTK3] Replace stock icon and text usage 2018-11-02 08:45:39 +00:00
0b39b529dd [PY3] Fix tray password encoding issue
The tray password need to be in bytes but GTK on Py3 returns unicode.

Use decode_bytes and then encode to ensure Py2/3 compatibility.
2018-11-02 08:45:39 +00:00
1d0e40c66b fix move_completed sensitivity 2018-11-02 08:45:39 +00:00
bcc89c73dd [GTK3] Fix statusbar clicking issues 2018-11-02 08:45:39 +00:00
a6b47e18c9 Fix package_data namespace in setup.py 2018-11-02 08:45:39 +00:00
5183c92543 [GTK3] Migrate to AppIndicator3
Replace the old appindicator imports with AppIndicator3.

- Only few changes required due to Enum renaming.
- Updated the preference import to include require_version and set a bool.
- The password preference needs to be encoded for hashlib on Python3 but
also need to keep Python 2 support so attempt decode then encode.
2018-11-02 08:45:39 +00:00
7c1c3f62d1 [Tests] Ensure GTKUI tests are skipped upon import errors
Need to catch any issues with importing GTK modules to ensure
GTKUI tests are skipped in non-GTK environments.
2018-11-02 08:45:39 +00:00
729f062ea1 [GTK3] Fix RadioMenuItem group error
Fixes a difference from GTK2->3 where the group can no longer be passed
as a RadioMenuItem so use get_group method to set group.
2018-11-02 08:45:39 +00:00
d879ee06a3 [Notifications] Migrate to GTK3
- Switch from pynotify to gi libnotify binding.
  - Ideally would drop libnotify for [GNotification] but requires more
    work with the desktop file needing renamed to DNS format.
    e.g. `org.deluge-torrent.deluge`

[GNotification]: https://developer.gnome.org/GNotification/

Co-authored-by: Calum Lind calumlind+deluge@gmail.com
2018-11-02 08:45:39 +00:00
ed1b2a50fa [Scheduler] Migrate plugin code to GTK3
- Added new svg icons for cleaner look.
- Use widget get_allocated_width and get_allocated_height instead of window size.
- Tweaked margins to fix spacing issues.
- Removed yellow background from 'slow settings' and applied only to label.

Co-authored-by: Calum Lind <calumlind+deluge@gmail.com>
2018-11-02 08:45:39 +00:00
c51e01ac46 [GTK3] Migrate plugins to GTK3
Add a new Gtk3PluginBase to prevent problems with Gtk2 plugins.
2018-11-02 08:45:39 +00:00
4df5bd05ec [GTK3] Replace stock icons for named icons 2018-11-02 08:45:39 +00:00
cf4012bb60 [GTK3] Fix and remove FIXME comments
- Several of the FIXME comments seem to be outdated so removed.
- The status_icon comment was resolved by fixing the arguments supplied to
tray_menu.popup().
- The TreePath no longer returns a tuple so cast path to int.
- Fix an error with Pixbuf signature.
2018-11-02 08:45:39 +00:00
bbcebe1306 [Tests] Fix use get_iter_first for treestore 2018-11-02 08:45:39 +00:00
bcaaeac852 [GTK3] Use explicitly named functions for creating menuitems
See https://wiki.gnome.org/Projects/PyGObject/InitializerDeprecations
2018-11-02 08:45:39 +00:00
4111f94597 [GTK3] Fix GError required attr id for GtkTreeSelection 2018-11-02 08:45:39 +00:00
dd7cc31918 [GTK3] Fix catching Wnck not available 2018-11-02 08:45:39 +00:00
d8d094cab6 [Travis] Fix missing pygi gtk3 packages 2018-11-02 08:45:39 +00:00
dc6e93541b [Docs] Fix mocking external modules
With move to GTK3 needed to update the mocking of external modules.

There is a new autodoc option `autodoc_mock_imports` so use this instead
of the custom mock class.

There are some build warnings output using autodoc mock:

    TypeError: unsupported operand type(s) for |: '_MockObject' and '_MockObject'

Will resolve these later as the build passes.
2018-11-02 08:45:39 +00:00
f6ffb940ab [Tests] Update tests for GTK3 new paths 2018-11-02 08:45:39 +00:00
6fbb1bb370 [GTK3] Fix ui_entry tests for gtk3 2018-11-02 08:45:39 +00:00
8285b226eb [GTK3] Fix Python 3 issues
In Python 3 builtin next function instead of the next method.

Unpickling with translated strings in state file causes ascii decode
error so ensure UTF-8 encoding is specified.
2018-11-02 08:45:39 +00:00
194129c027 [GTK3] Fix create torrent dialog warning
The ui liststore had an extra blank value causing a GTK value type
warning.

Also set the default value displayed to be 128 KiB.
2018-11-02 08:45:39 +00:00
7d5a429466 [GTK3] Fix column lookup not i18n-ised 2018-11-02 08:45:39 +00:00
ac5db1b262 [GTK3] Fix gettext translation code
Add translation setup for Gtk.Builder ui files.

Refactor and cleanup up the translations_util:
- Remove old gtk.glade code.
- Add macos libintl support.
- Remove unneeded setup_translations parameters.
2018-11-02 08:45:39 +00:00
a2857a318d [GTK3] Remove unneeded code in path chooser
Not sure the reason for the added introspection code but originated in
GTK3 changes so remove as path chooser doesn't appear to need it.
2018-11-02 08:45:39 +00:00
13e1fa355d [GTK3] Fix path chooser warnings and errors 2018-11-02 08:45:39 +00:00
2e88fa1dfc [GTK3] Remove listview orphaned code
Not sure why/where this originated from but seems to serve no purpose so
removing it.
2018-11-02 08:45:39 +00:00
366b10f07b [GTK3] Fix displaying column popup menu
Right-clicking on column header resulted in this error:

    TypeError: could not convert type EventButton to GdkEvent required for parameter 0

The following fixes and cleans up the issue:
- Move the signal creation to the class, using the __gsignals__ dict.
- Replace `Event` with `object` since we are passing an EventButton as
Gtk3 no longer accepts it as an Event.
- Replace deprecated menu `popup()` with `popup_at_pointer()` which also
fixes a critical gdk error when using `popup()`.
2018-11-02 08:45:39 +00:00
92a048625a [GTK3] Fix the transient parent for PathChooser
The filechooser dialog was wrongly transient to the main window causing
weird behaviour, namely the main window moving but dialog remaining in
place when attempting to move the child dialog.

The solution is to pass the parent dialog to PathChooser so it can be
properly set the filechooser dialog transient property.

Fixed the Preferences dialog not being set to be modal to main window.
2018-11-02 08:45:39 +00:00
8199928160 [GTK3] Use a non-CSD filechooser dialog for PathChooser
My personal feeling is that GTK client-side decoration (CSD) putting
main dialog buttons in the titlebar is wrong so create a non-CSD dialog.

There was no simple way of changing GtkFileChooserDialog to play nice
with non-CSD buttons and resulting in these GTK warnings:
    Gtk-WARNING : Content added to the action area of a dialog using header bars

There is an unwanted dialog border with this custom filechooser dialog
with no apparent way to remove them. Would require switching to a
GtkWindow implementation.
2018-11-02 08:45:39 +00:00
545aca9a4c [GTK3] Fix piecesbar Pango methods
Fixes 'Context' object has no attribute 'create_layout'
Fix the gi cairo import warning
2018-11-02 08:45:39 +00:00
9f113eab23 [GTK3] Fix ImportError has no attribute message
Python 3 exception objects now use `msg` but casting with `str()` is
better.
2018-11-02 08:45:39 +00:00
bc6bc017cb [GTK3] Fix TextBuffer.get_text() arguments
This fixes an error that it takes exactly 4 arguments (3 given)
2018-11-02 08:45:39 +00:00
535fda90e3 [GTK3] Use core.pause_torrents with list of torrents
Use the new api method.
2018-11-02 08:45:39 +00:00
0ace086de4 [GTK3] Fix About 'MainWindow' object has no attribute 'get_window' 2018-11-02 08:45:39 +00:00
bbb1b44a23 [GTK3] Fix piecesbar window attribute error
Fixes the error: object has no attribute window
2018-11-02 08:45:39 +00:00
dfed17ac0d [GTK3] Fix import and attribute warnings
Fixes:
- warning import GConf
- Gtk is not defined
- 'ResponseType' has no attribute 'Yes'
- selection.data should be selection.get_data
2018-11-02 08:45:39 +00:00
2f879c33f3 [GTK3] Fix path_combo_chooser warnings
Fixes
 - Please remove the widget from its existing container first
 - object has no attribute 'height'
2018-11-02 08:45:39 +00:00
14b6ba10cf [GTK3] Use decode_bytes from method returns
Python3 PyGObject automatically encodes/decodes strings to and from
methods. This does not happen on Python 2 so for compatibility use
decode_bytes.
2018-11-02 08:45:39 +00:00
ae0b072b75 [GTK3] Fix config default_load_path keyerror 2018-11-02 08:45:39 +00:00
250afa6e0b [GTK3][OSX] Restore windowing lookup for quartz 2018-11-02 08:45:39 +00:00
b29b6fe69f [GTK3] Restore and update expand_row code 2018-11-02 08:45:39 +00:00
f160d6312f [GTK3] Fix piecesbar warnings
Fixes:
- get_style to get_style_context
- use str with gsignals
2018-11-02 08:45:39 +00:00
a8d01fd52f [GTK3] Fix GObject deprecation warnings
GObject.idle_add is deprecated using GLib.idle_add instead
GObject.timeout_add is deprecated using GLib.timeout_add instead
GObject.SIGNAL_RUN_LAST is deprecated; use GObject.SignalFlags.RUN_LAST instead
GObject.GError is deprecated; use GLib.GError instead
GObject.timeout_add is deprecated use GLib.timeout_add instead.
ListStore(str, str) using unicode_literals get_value return utf8.
2018-11-02 08:45:39 +00:00
3a5ec4f5f4 [GTK3] Fix Glade ui margins and spacing 2018-11-02 08:45:39 +00:00
eebb93d4ee [GTK3] Cleanup Glade UI deprecated widgets and properties
- VBox is Box(vertical);
- HBox is Box;
- HButtonBox is ButtonBox;
- HSeparator is Separator;
- VPaned is Paned(vertical);
- Table is Grid;
- use_action_appearance is purged;
- yalign is purged;
- xalign is purged;
- xoptions are purged;
- yoptions are purged;
- add some align: start;
2018-11-02 08:45:39 +00:00
01fafd4fe0 [GTK3] Change module structure from ui/gtkui to ui/gtk3
This moves the directory structure so that there is no conflict with the
old gtk2 UI. Also changes the conf and state files being loaded.
2018-11-02 08:45:39 +00:00
ca0db4d1a7 [GTK3] Save ui files with Glade 3 2018-11-02 08:45:39 +00:00
ea72164798 [GTK3] Large rename/modify code for GTK3 2018-11-02 08:45:39 +00:00
e2ba980299 [GTK3] Marked FIXME code changes for GTK3 2018-11-02 08:45:39 +00:00
98051bdea2 [Docs] Move apidoc command to Sphinx config
The apidoc modules were not being generated on ReadTheDocs because
there is no way to run sphinx-apidoc manually.

Moved the running of sphinx-apidoc into conf.py.

Added zope.interface minimum version to fix Readthedocs warning.
2018-11-02 00:21:50 +00:00
20431cc771 [Docs] Fix build errors getting Deluge version
The use of pkg_resource.require caused an unwanted requirements lookup
that errored out the sphinx build when no dependencies are installed.

This is fixed by switching to pkg_resources.get_distribution.

Also changed the tox docs env to not install Deluge as the setup.py
now contains install_requires which is unwanted.
2018-11-01 23:18:05 +00:00
82ecf8a416 [Docs] Reorganise and add sections from wiki
- Change the layout and contents of docs to be better organised and
  follow ideas from: https://www.divio.com/blog/documentation/
- Use markdown for non-technical documents to speed up writing.
- Added new sections and imported documents from Trac wiki.

Build fixes:

- Added a patch to fix recommonmark 0.4 and doc referencing:
    https://github.com/rtfd/recommonmark/issues/93
- Set docs build in tox to Py2.7 since there are problems with autodoc
  mocking multiple inheritance on Python 3 resulting in metaclass errors.
- Supressed warning about `modules.rst` not in the toctree by creating
  a static `modules.rst` with `:orphan:` file directive and add to git.
  Also skip creating this toc file with sphinx-apidoc in setup and tox.
- Simplified finding exported RPC and JSON API methods by adding an
  autodoc custom class directive. Removed unneeded __rpcapi.py.
2018-11-01 17:38:10 +00:00
9dcd90056d [Lint] Fix flake8 warnings
- Use six to silence flake8 undefined Python 2 names on Python 3.
- Fix W605 invalid escape sequence.
- Cleanup unused exception variables.
2018-10-25 15:14:19 +01:00
e2c7716ce2 [#1903|UI] Add super seeding option to interfaces
- Fix applying the setting to libtorrent, passing the value without
  modification so it decide when to enable it.
- Enable super_seeding option when adding torrents to core.
- Update UIs with option in tabs and add dialogs.
2018-10-25 15:14:07 +01:00
e6c61c3f8c [Core] Fix potential renaming unicode folders issue
- Found an issue while fixing `get_name` where `handle.rename_file`
  would raise a UnicodeDecodeError with non-ascii on Python 2. The
  fix is to catch this and pass unicode string to method instead.

- Add a test `test_rename_unicode` to verify no errors are generated.
- Updated test to use core.session instead of creating another one.
2018-10-22 21:58:06 +01:00
b834e33568 [#3204|Core] Fix unicode get_name unicode error
The recent change to torrent.get_name does not handle non-ascii paths
on Python 2.

- Add a decode_bytes to resolve the issue.
- Add tests.
- Refactor to reduce nesting.
2018-10-22 21:58:05 +01:00
9ab2a50097 [Console] Refactor single letter variables
- Replace usage of `s` for variable names to make it easier to read the code.
- Remove unneeded and unused encoding parameter from parse_color_string
  It should not be encoded by this function, only on output.
2018-10-22 16:05:55 +01:00
1838403e3b [#3199|Console] Fix UnicodeEncodeError in addstr
The following error was encountered by user:

   ...deluge/ui/console/modes/basemode.py, line 290, in add_string
       screen.addstr(row, col, string, color)
   UnicodeEncodeError: 'ascii' codec can't encode character...

The `add_str` method is defaulting to using the Python 2 ascii
encoding with a unicode string so use the encoding passed to the
function.
2018-10-22 16:00:12 +01:00
9e7c9fc1d3 [AutoAdd] Fix packaging autoadd_options data files
There was a warning about missing javascript file in the package and
found the `data/autoadd_options` subdir was not being included.

Bump version
2018-10-21 16:05:01 +01:00
9264cb749e [WebUI][#2009] Add About window
- Add an About window to see version details like GTKUI.
- The author and license text were left out as unnecessary.
- Added a daemon get_version method since daemon version was not
  available through the json-api.
- Fix LookupResource to ensure path exists when rendering.
2018-10-21 15:43:29 +01:00
c01679de1f [Config] Prevent symlinked config files being overwritten
If a user keeps the deluge config file under source control and symlinks
to the config files in deluge config dir then when deluge saves config
files it will replace the symlink with actual file.

Using realpath will resolve these symlinks and file will be updated in
the correct location.

Use a temporary file for new config new before moving it to the
resolved location.

Co-authored-by: Calum Lind <calumlind+deluge@gmail.com>
2018-10-21 14:08:30 +01:00
3645feb486 [GTK] Fixup translation strings
- Use named placeholders to allow translators to change text order.
- Refactor fpcnt to use format() and markup strings properly.

- Remove gettext.js creatation from generate_pot script since this step
  is now always done at build-time.
2018-10-21 13:51:30 +01:00
d85f665091 Fix large ETA overflow C int
The following error was encountered in GTK3 which is a result of trying
to cast a very large ETA value to C int and raising an Overlflow error.

   <type 'exceptions.OverflowError'>: 3072227291 not in range -2147483648 to 2147483647

The solution is to limit the ETA to 1 year and represent any values over
that as -1 which the UIs can display as infinity.
2018-10-19 17:30:27 +01:00
5ec6ae3ad0 [Packaging] Minimal requirements for test_requires in setup.py
Remove extra_requires since requirements-*.txt files provides these now
plus the extras_requires is for extras at installation time which does
not apply to docs or dev.

For test_requires include the minimal requirements for pytest to run.
This is not the same as the longer tox test requirements that include
linting, docs etcs.

Fix license field.
2018-10-19 14:14:03 +01:00
860730d43c [Packaging] Update make_release script for Py3
xz compression is included in Python 3 so simplify script.
2018-10-19 14:14:03 +01:00
c1ddcf6012 [Packaging] Add install_requires to setup.py
- Add an install_requires list to allow dependencies to be automatically
  installed via setuptools or pip installation.
- Needed a workaround for twisted service_identity install.
2018-10-19 14:14:03 +01:00
85bbdfe143 [Packaging] Cleanup dependencies
- Tweaked the layout a bit with optional part of dependency description.
- Updated descriptions to help understand dependency usage.
- Made intltool and chardet packages optional. This will help with
  installation where these might be missing and are not crucial.
- Remove gettext from dependency as is part of Python.
2018-10-19 14:14:03 +01:00
9f9827ca58 [Packaging] Fix appdata.xml details
- License should be GPL3+.
- Use the Deluge HTTPS URLs.
- Wrap the description long lines.
- Add launchable tag to launch from software centre after install.
- Removed name tag to use .desktop file name field instead.
2018-10-19 14:14:03 +01:00
dcb3dad435 [Core] Fix renaming folder not updating torrent name
The new get_name method needs to function as it did in 1.3-stable so
when renaming a torrent file or top-level folder update torrent name
to reflect that. If UI supports renaming the torrent then the options
value with be used instead.
2018-10-19 14:02:36 +01:00
0e69b9199c [GTK] Fix toggling auto_managed in Options tab
The refactoring of the options code didn't update to use the new status
key `auto_managed` and `is_auto_managed` is not a valid torrent option.

Related: 1637da84e4
2018-10-19 14:02:22 +01:00
88a3600ce3 Fix sdist missing test files
The exclude 'deluge.tests' in setup.py is for excluding from package
installation but we want the files when distributing as source so
include the entire deluge/tests dir (excluding .pyc files).
2018-10-16 14:59:00 +01:00
91164d8dbf Cleanup and use markdown for source text files
Use markdown to aid readability.

Update the README and use it for the long_description in setup.py
Add detailed requirement information to the DEPENDS files.
2018-10-16 14:59:00 +01:00
ec47720686 [WebUI] Enable debug URL parameter to parse false values
When installed as a development version there was no way to load the
normal js scripts so improve the debug arg handling by parsing for false
values to force use of normal type scripts. Since debug arg overrides dev,
leave dev as is.
2018-10-16 11:52:04 +01:00
467ade1eb7 [WebUI] Fix closure minified size increase
All of the non-standard docstring file headers were being added to
minified files, increasing the file size. Replace with jsdoc `/**`.

Remove ext-extensions from git as will be generated by minify script.
2018-10-16 11:38:13 +01:00
bb93a06fff [WebUI] Fix prettier javascript issue
Prettier removed too many parentheses in asIPAdress and when minifying
the plus unary combined with the plus operator to create an incorrect
increment operator. So skip prettier formatting this secion of code.

Also fix an ECMA5 warning from closure about function scope in FilesTab.
2018-10-16 11:37:11 +01:00
80178f7310 Update javascript minifying script
- When both minifying modules are missing, creating a copy of the debug
  file is not actually desirable, a missing file is more obvious than a copy.
  WebUI can handle a missing 'normal' script and fallback to 'debug' script so
  modified script to skip and warn instead.
2018-10-16 11:34:55 +01:00
ee354eb107 [WebUI] Keep debug js in packaging and fix script lookup
Packaging:

- Decided that the debug files are useful for end-user so keep them in
  package installation. For debug script_type to be usable all debug
  file need to be avaialble so extjs debug files also included.

Script type selection:

- Fixed dev and debug request args to be properly decoded on Python 3,
  otherwise comparison would fail and allowed any case for values.

- Modified the choosing of the script type to pick debug if specified
  as previously always choosing dev type if dev version was True. A rare
  scenario but useful but now debug is used if specified otherwise use dev.

- Changed the order when looking for alternative script types to start
  with dev so that if debug is specified but missing it uses a similar
  script type as previously would fallback to normal which is likely
  undesired.
2018-10-16 11:29:41 +01:00
7d896599b8 [GTK] Fix speed appearing in blanks cells
In TorrentView and PeersTabView, when moving the mouse pointer through
rows, the value in download and upload speed columns can be overwritten
by value from previous row cell.

The solution is to disable return by cached condition in function
cell_data_speed.

Discussion: https://github.com/deluge-torrent/deluge/pull/200#issuecomment-424907571

Removed debugging code.

Co-authored-by: Calum Lind <calumlind+deluge@gmail.com>
2018-10-14 10:52:08 +01:00
55aee2b00f [Common] Fix incorrect path in TorrentInfo
Refactoring for Python 3 did not account for the `self._files` using an
updated info_dict so simplify code by updating in the files for loop.

Added test that TorrentInfo.files returns the correct structure.
2018-10-14 10:51:08 +01:00
10d39c83cb [WebUI] Fix error escaping translated text
In a previous commit d4023e7dde removed a decode for Python 3 but
with translated text returned by gettext encoded on Python 2 the
escape function would raise a UnicodeDecodeError trying to use ascii
to decode.

The fix is to decode the message returned by gettext on Python 2.
2018-10-13 22:57:28 +01:00
0b2cb7539f Cleanup Tox and CI configs
- Use the apt addon for installing libtorrent package.
- Start the py3 test sooner as it is slow to complete.
- Add if conditions for gtkui test dependencies.
- Remove Appveyor tests that are taken care of by Travis.
2018-10-10 18:50:41 +01:00
6fdbf0ba5d Update tox and CI for Python 3 2018-10-10 17:57:02 +01:00
a980f8e959 [WebUI] Allow multiple torrent uploads in Add dialog
Add a new `multiple` field to FileUploadField to allow selecting
multiple files. Include a fallback for if browser does not support
multiple file selection.

Update Add window to upload and parse multiple torrent files at once.
2018-10-10 17:57:02 +01:00
c90cf301df [WebUI] Use application/json in header 2018-10-10 17:57:02 +01:00
6f06cd5ebc [WebUI|Py3] Refactor content_type check
Simplify getting content_type from request to prevent str/bytes mixup.
2018-10-10 17:57:02 +01:00
86de5657ff [WebUI|Py3] Fix and refactor torrent upload 2018-10-10 17:57:02 +01:00
4a335eeb61 [Tests] json loads Python 3.5 compatible
json.loads in Python 3.6 accepts str or bytes but Python 3.5 is str
only so decode.
2018-10-10 14:41:10 +01:00
86d582d52a Remove debugging log line 2018-10-10 14:41:10 +01:00
673b6653a3 [Py3] Fix TorrentInfo info_dict decoding 2018-10-10 14:41:10 +01:00
41732fe38b [WebUI|Tests] Fix json_api tests for Python3 2018-10-10 14:41:10 +01:00
5964bcf897 [Tests|Py3] Fix prefetch metdata test 2018-10-10 14:41:10 +01:00
3ed4a6e834 [WebUI] Fixes for login auth on Python 3
Remove obsolete password check code.
2018-10-10 14:41:10 +01:00
20fa106b8b Update pre-commit config
The prettier hook was missing a trailing slash so omitting css files.

Add a trailing space fix hook and fix issues.
2018-10-08 14:49:36 +01:00
654e2af4e5 [WebUI] Fix browser Flash plugin warning
Do not perform flash player version detection using the fix from here:
  https://github.com/georchestra/georchestra/issues/902
2018-10-08 14:47:27 +01:00
d5dea44689 [WebUI] Update extjs to 3.4.1.1
Better late than never...

http://cdn.sencha.com/ext/commercial/3.4.1.1/release-notes.html
2018-10-08 14:47:27 +01:00
5743382c65 Remove Pipfile and pipenv
For now using requirements files and tox to setup a dev env so to
prevent confusion or stagnation of Pipfile remove it and pipenv
documentation.
2018-10-08 12:21:00 +01:00
39f37e6133 [Tests] Remove slimit dependency
The changes to the minify script mean we no longer require slimit.
2018-10-08 12:19:03 +01:00
0ed3554f95 [WebUI] Copy non-minified JS file if slimit missing
This will remove the setup dependency in "slimit" package.
In case "slimit" is missing, the non-minified JS files will be copied
as is to the build.
"slimit" is marked as a dependency for development process only.
2018-10-08 12:13:21 +01:00
ba6af99b05 [Notifications] Set notification desktop entry hint
Due to Gnome Guidelines https://wiki.gnome.org/Initiatives/GnomeGoals/NotificationSource
2018-10-08 12:11:07 +01:00
9e29fe4111 [Tests] Lint with pre-commit
- Add lint section to tox.
- Replace flake8 with lint on Travis and remove commented out sections.
- Remove flake8 from appveyor to reduce sequential testing time.
2018-10-05 18:45:37 +01:00
a8a4fb69c0 [Lint] Exclude js and css from EOF fixer
- When running pre-commit on all files it is picking up minified js and
  css files. Since prettier will format correctly the source files ignore
  them in end-of-file fixer.
- The template files in web docs can be ignored too.
- Removed the unneeded `pre-commit-hooks` dependency as pre-commit
  resolves that itself.
- Include files fixed by pre-commit.
2018-10-05 18:45:37 +01:00
6cf13d112b [Tests] Remove debug traceback 2018-10-05 13:23:45 +01:00
6973f96f8c [Tests] Update tox and CI configs
- Add new requirements files to make it easier to install deps.

- Tox changes
  - Update tox to use new requirements files.
  - Tweak heading styles.
  - Add development environment command `devenv`.
  - Remove testenv command as it would run on devenv creation.

- Travis changes
  - Now uses xenial as trusty is very old now.
  - Trial run disabled to speed up tests.
  - Add tox-venv for Python 3 support.
  - Only install testssl if running security tests.

- Appveyor
  - Add tox-venv for Python 3 support.
  - Use requirements file for non-tox.
  - Remove trial run to speed up testing.
2018-10-05 13:23:45 +01:00
0548bdb655 [Lint] Add pre-commit config
- Added a pre-commit config for code linting and formatting. It will
  auto-format python, javascript, CSS, YAML and markdown files to save
  manually doing so. To install:

      pip install pre-commit
      pre-commit install

- Added a default virtual environment directory to gitignore.
2018-10-05 09:45:42 +01:00
36606fc448 [Docs] Add markdown support
- Use recommonmark to enable use of markdown files in docs.
- Fix theme not specified
- Remove unused spelling module.
- Cleanup mocking modules in conf so building docs requires only Sphinx.
- Simplify tox section, including use of requirements-docs file. Added
  slimit dependency for sdist-ing deluge package.
2018-10-04 15:53:42 +01:00
c415b097fe Cleanup outgoing_interface code and help text
- Remove is_ip check as libtorrent does accept IP address for this setting.
  See: https://github.com/arvidn/libtorrent/issues/3087
- Use consistent wording for help text.
2018-10-04 10:51:20 +01:00
970fad7557 [GTK] Fix column name missing translation markup
With non-English languages this lookup would fail without gettext
translation of the column name.

A better solution is to not use the translatable column title as an
index but this is a quick fix for now.
2018-10-04 10:39:48 +01:00
358ff74d0e [Lint] Format files with Prettier
Use Prettier to auto-format javascript, CSS and YAML files so that less
manual work is involved and style is consistent across project.
2018-10-03 18:16:09 +01:00
b1cdc32f73 [Lint] Use Black to auto-format code
The move to using auto-formatter makes it easier to read, submit and
speeds up development time. https://github.com/ambv/black/

Although I would prefer 79 chars, the default line length of 88 chars
used by black suffices. The flake8 line length remains at 120 chars
since black does not touch comments or docstrings and this will require
another round of fixes.

The only black setting that is not standard is the use of double-quotes
for strings so disabled any formatting of these. Note however that
flake8 will still flag usage of double-quotes. I may change my mind on
double vs single quotes but for now leave them.

A new pyproject.toml file has been created for black configuration.
2018-10-03 15:21:53 +01:00
bcca07443c [Common] Fix config missing value assignment 2018-09-30 14:58:11 +01:00
67d9c2efb4 [Core] Fix saving listen_interface as None
A mistake in refactoring meant that listen_interface was reset to None
on shutdown.
2018-09-30 14:58:03 +01:00
34b0fdff1d [Core] Retain magnet details when loading state
It is useful to keep the magnet uri even with the torrent_info. When
adding the torrent magnet details are only used if torrent_info is not
available.
2018-09-28 15:01:34 +01:00
f93e5e60b5 [Core] Refactor session status code
Simplify the methods by initialising the session_status dict using
libtorrent session_stats_metrics and rate keys.

Instead of first looking for deprecated keys use exception then lookup.

Added a few more tests.
2018-09-28 15:01:34 +01:00
d8b1e2701c [#3080] Fix torrent reappearing on restart
If a magnet is added to new Deluge state, then deleted, it will reappear
on restart.

The problem results from torrents requiring both state and torrent file
but magnet only rely on the state file and the save_state code not
saving if the torrent list is empty. So torrents won't be loaded as
their torrent files have been deleted but magnets details remain in
state file and are loaded again on restart.

The fix is to always save the state file even if the state is empty.
2018-09-27 10:56:58 +01:00
abf4c345f0 [#2398|GTK] Update prefetching magnet metadata in AddTorrent dialog
The new code automatically attemps to fetch magnet file details from
core while displaying a 'waiting' message.
2018-09-26 14:18:52 +01:00
a09334e116 [GTK] Refactor AddTorrent dialog update config scoping
- Fix a potential scoping issue with callback function by moving to
class method and adding requried parameters.
- Remove unneeded return statements and variable.
2018-09-26 14:18:52 +01:00
57ad9a25da [GTK] Fix AddTorrent dialog title count
Use the treemodel signals to keep the torrent count in the dialog
title correct.
2018-09-26 14:18:52 +01:00
5a2990ff90 [Core] Handle already prefetching magnet metadata 2018-09-26 14:18:52 +01:00
759a618f74 [Core] Tweaks to prefetch metadata method
- Disable the magnet from being auto_managed so it starts immediately.
- Reduce the default timeout to 30secs.
- Use the generic tempfile dir.
- Move callback method to be class method
2018-09-26 14:18:52 +01:00
23f1cfc926 [Core] Assign magnet arg if magnet URI in filename
A magnet that prefetched the metadata is added as a normal torrent but
with the filename set to the magnet URI. So we need to assign the URI to
magnet arg and set filename to None before creating Torrent object.

Updated the is_magnet function to prevent AttributeError with None type.
2018-09-26 14:18:52 +01:00
57ea5ef5da [GTK] Set default file priority to 4 in add dialog 2018-09-26 14:18:52 +01:00
944dc1659f [Core] Fix UI file priorities out-of-sync with libtorrent
The file priorities were not updating correctly in the UI and it was
found that in lt 1.1 file priorities are now updated asynchonously so we
cannot get the values immediately. So only update the options
file_priorities if they are empty.
2018-09-26 14:18:52 +01:00
2dc157578e [GTK] Refactor torrent_liststore appending in AddTorrentDialog
The add_from_torrent and add_from_magnet shared common code so refactor
into methods for reuse.
2018-09-26 11:08:30 +01:00
8a59216061 [#2398|GTKUI] Fetch magnet files details in Add Dialog 2018-09-26 11:08:30 +01:00
cc1807cf97 Fix travis docs failing with Sphinx 1.8.0
Sphinx pinned to 1.7

See sphinx-doc/sphinx#5417
2018-09-14 16:09:20 +01:00
63b7f6d382 [Tests] Add pytest-twisted to tox deps 2018-09-14 16:06:06 +01:00
5c4cbf58c5 [Tests] Fix UnicodeWarning for gzipped file comparison 2018-09-14 16:06:06 +01:00
5959a24d4c [Lint] Flake8 cleanup 2018-09-14 16:06:06 +01:00
d4023e7dde [Py2to3] More fixes for web ui 2018-09-14 16:06:06 +01:00
0fd3c25684 [Py2to3] Fixes to display Web UI 2018-09-14 16:06:06 +01:00
4125e35ebd [Py2to3] Fix test_tranfer strings should be bytes 2018-09-14 16:06:06 +01:00
18d448d4a5 [Py2to3] Ensure httpdownloader saves data as UTF-8
Python 3 raised a decoding error with the google page which appears to be
encoded with 'latin-1', so extract the content charset to decode and
re-encode in 'utf-8'.
2018-09-14 16:06:06 +01:00
d5133f789a [Py2to3] Fix opening torrent files in byte mode 2018-09-14 16:06:06 +01:00
1cce6a297c [Py2to3] Further maketorrent fixes 2018-09-14 16:06:06 +01:00
ad20ec62f2 [Py2to3] Fix TorrentInfo metainfo dict key lookups 2018-09-14 16:06:06 +01:00
af2bed8a0f [Py2to3] Fix tests for maketorrent and metafile 2018-09-14 16:06:06 +01:00
b93e868048 [Py2to3] Fix ui_entry default indicator 2018-09-14 16:06:06 +01:00
8d90ae5ffb [Console] Fix cmdline output and tests 2018-09-14 16:06:06 +01:00
ae4449642c [Py2to3] Fix log.warn deprecation warning 2018-09-14 16:06:06 +01:00
bc2f4a30eb [Py2to3] Fix putChild requires bytes 2018-09-14 16:06:06 +01:00
dc8766874e [Tests] Fix testing core.add_torrent_url on Py3 2018-09-14 16:06:06 +01:00
a33171732d Upgrade Pipfile lock for twisted
Due to a security warning for cryptography update twisted and deps.
2018-09-14 10:54:22 +01:00
b9a9e06c1d [WebUI][Daemon] Enhance TLS Security
This applies the following for both WebUI and Daemon:
1. Raised minimal TLS version to TLSv1.2
2. Added specific cipher suite list
3. Added support for ECDSA auth keys
4. Added support for ECDHE key exchange algorithm

We disabled the ability to perform TLS/SSL renegotiation and therefore
will prevent the clients from renegotiating, which can be exploit for
DoS attacks.

New security tests now will be skipped when running `pydef` and `trial`
testenvs. To run the test, use the testenv `security` or add the environment
variable `SECURITY_TESTS` before running the tests. Also should only run when
adding to the commit message the string `SECURITY_TEST`.
2018-09-06 19:14:13 +01:00
456e720b75 [WebUI] Constrain large icons to fit sidebar properly
Add CSS background position and size for sidebar icons.

Without this change tracker icons that are too big, render as too big.
This restrains them to fit into the sidebar list item.
2018-09-06 18:57:28 +01:00
ae9bbdbae7 [Core] Add tests for pausing and resuming torrents 2018-08-10 17:48:53 +01:00
585ea88f1f [Core] Fix torrent pause/resume logic
If the torrent_id argument received in the pause or resume methods is not a string, the methods execute with the un-parsed input and then with parsed input on a second call. A key error exception is thrown from the first call, and the second call succeeds.
2018-08-10 17:09:50 +01:00
f94f58918e [Core] Remove libtorrent deprecated resolve_countries
Libtorrent 1.1 no longer supports this so remove it.
2018-08-10 09:56:38 +01:00
3fc97672de Fix the docs run failing on Travis
Likely that the deprecation warning from cryptography is causing the
setup.py sphinx build command to return an error so the tox/travis job
is marked as failing. Changing to calling the sphinx-build command
directly solves this.

Also updated the sphinx config for built-in napoleon and faster builds
using jobs option.
2018-07-28 10:26:02 +01:00
e8e649a030 Prevent time formatting crash when seconds were floats
Update docstring and tests for ftime supporting floats

Truncate rather than round floats in ftime
2018-07-27 07:26:37 +01:00
1e6c02ae83 [Core] Fix get_eta returning float instead of int
Floor division will return a float if a float is provided so ensure int
when dividing by the stop_ratio. All other status values from libtorrent
are ints.

Added tests.
2018-07-16 16:25:08 +01:00
b2e1f850d8 [WebUI] Handle missing gettext.js file
Removed the creation code of `gettext.js` and now it will just mock
the `_` function by being the identity function.
2018-07-16 16:22:42 +01:00
8bfa2cacbb Cleanup docstrings in httpdownloader
Use the new google docstring style.
Keep line length to 80 chars and new lines for mult-line func params.
2018-07-15 11:58:16 +01:00
c7e61f8c34 [HTTPDownloader] Refactor HTTPDownloader to Agent
As of twisted 16.7.0, `twisted.web.client.HTTPDownloader` have been
marked as deprecated.
This caused the tests results to show many lines of warnings about it.
This refactor uses `twisted.web.client.Agent`, as suggested by Twisted.
2018-07-15 11:58:16 +01:00
089c667d7f [AutoAdd] Add WebUI interface 2018-07-12 19:18:08 +01:00
ebb955934d [Console] Fix unhandled error in preferences
If the value is None then the len cannot be calculated so set to blank
string.
2018-07-12 16:46:35 +01:00
c7567ddee4 [Console] Fix unhandled error in torrentactions
Fix calls to core pause and resume using the singular methods with a
list of torrent ids.

Fix and simplify the handling of errors from core.
2018-07-10 16:46:49 +01:00
c655da38c8 [Console] Ensure time string is unicode 2018-07-10 16:46:49 +01:00
4c0be7ddd4 [Common] Fix missing return for de/encode methods 2018-07-10 16:46:49 +01:00
38961d4253 [Console] Fix char encoding in preferences 2018-07-10 16:46:49 +01:00
6e81a11d8d [Console] Fix and refactor window.add_str code
- Replace usage of single char variable.
- Use max function to prevent negate lengths when calculating trim.
- Remove usage of insstr as the addstr exception when writing offscreen is
excpected behaviour so needs caught and passed. This fixes a similar
error with insstr.
2018-07-10 16:46:49 +01:00
be02be75be [GTKUI|Py3] Ensure backward compatible pickle dumps 2018-07-01 20:34:42 +01:00
7b5ed9f1d6 [Py3] Fix dict iter item deletion 2018-07-01 10:08:16 +01:00
e626f9fece [Win32] Fix missing certs for HTTPS requests
The following error occured on Windows when switching to using HTTPS
url with Twisted Agent:
```
<class 'twisted.web._newclient.ResponseNeverReceived'>: [<twisted.python.failure.Failure OpenSSL.SSL.Error: [('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')]>
```

The fix is to install certifi and provide the path to the trust store as
env var for OpenSSL to pick up.

Also includes a simplication of the core test_listen_port code.
2018-06-29 10:29:30 +01:00
3fab799dbf Fix flake8 trailing comma 2018-06-27 16:41:21 +01:00
24c100d9b7 [Py3] Decode new_release version to unicode string 2018-06-27 16:41:21 +01:00
9bc2f62c80 [Py3] Fix tranfer header first byte check
The index of a byte in Python 3 will return an integer so use slice for
compatibility with 2/3
2018-06-27 16:41:21 +01:00
1fa2de066f Fix mistakes in test code 2018-06-27 16:41:21 +01:00
ae0b070c1b [Py3] Fix sort and sorted issues
In Python 3 there is only the key functions available and cmp functions
should be removed, especially for speed.
2018-06-27 16:41:21 +01:00
c3a2c67b98 [Py3] A large set of fixes for tests to pass under Python 3
The usual minor fixes for unicode/bytes for library calls.

The minimum Twisted version is now 16 for Python 3 support so remove old
code and start replacing deprecated methods.

Raised the minimum TLS version to 1.2 for the web server.
2018-06-27 16:41:21 +01:00
200e8f552b Add dependecy on six 2018-06-27 16:41:21 +01:00
4247013446 [bencode] Fix errors with unicode dict keys or values 2018-06-26 12:42:26 +01:00
6ec32a85e4 [GTKUI] Fix AttributeError with clipboard strip 2018-06-26 12:42:26 +01:00
633c56f54e [Core] Add prefetch metadata methods for magnets 2018-06-26 12:42:26 +01:00
23171ad205 Fix missing _metainfo attribute in TorrentInfo 2018-06-26 12:42:26 +01:00
277576268c [Py2to3] Replace deprecated base64.(de|en)codestring
* In Py3 base64.encodestring is deprecated so rather than use the
   Py3 only encodebytes instead use b64encode. The other advantage is
   that with issue a consistent TypeError is raised that we can catch.
2018-06-26 12:42:26 +01:00
74aa0db956 Manually compress png with zopflipng 2018-06-21 14:50:19 +01:00
fe42fb2c31 Recreate and compress icons
Will the change to deluge.svg all the icons need recreated. I also
updated the script to losslessly compress the png files with zopflipng.

hicolor theme changes:
 - Added a 512px icon.
 - Added a deluge-panel.png for systray theming.

Added extra webui icons and updated index.html to use them
correctly.
2018-06-21 14:50:19 +01:00
4973538d6c Tweak the deluge icon size
At smaller sizes the padding makes the icon too small so only keep the
padding at the bottom and let the point of the droplet extend to top of
canvas.
2018-06-21 14:40:52 +01:00
de1e7c27df [#2867][WebUI] Fix Daemon connection problem
Trying to connect to daemon B while still connected to A will cause
the torrents from A to be shown after connecting to B.
Therefor, checking if connected to any daemon before connecting to B.
2018-06-19 09:39:15 +01:00
587b9afefe [GTKUI] Fix count of torrents in dialog title 2018-06-19 09:34:46 +01:00
63b25311f5 [UI] Refactor TorrentInfo and add functionality
Make a clearer distinction about torrent metainfo and metadata and allow
passing these to TorrentInfo.
2018-06-19 09:12:57 +01:00
d45dbfe064 [Core] Add is_session_paused method 2018-06-18 20:06:35 +01:00
3176b877a4 [Core] Add methods pause_torrents & resume_torrents 2018-06-18 20:06:35 +01:00
18541bce86 Fixed a minor grammatical error in the deluge console command documentation 2018-06-18 20:04:22 +01:00
bebe08d92b Add X-GNOME-UsesNotifications
Due to Gnome Guidelines https://wiki.gnome.org/Initiatives/GnomeGoals/NotificationSource
2018-06-18 20:00:49 +01:00
0dbbb51cff [Core] Fix strip None in set_listen_on
Trying to strip None will not work so combined the check as a condition
for the strip.
2018-06-18 19:56:16 +01:00
bd78bd2643 [#3001|GTK] Fix sorting to default to Added column
The sort function used when no column is being sorted was a lambda
function that had no effect and had a bug. Instead default to sort by
the added date.

Also fixed the name column sort to lowercase and uppercase properly.
2018-06-17 08:34:17 +01:00
7a3b164060 [WebUi] Fixed Install Plugin window creation 2018-06-12 03:50:30 +03:00
e7eb26416e [Core] Fix strip None in outgoing_interface
Trying to strip None will not work so do this after checks for
falseness.
2018-06-09 22:18:38 +01:00
b2b7703081 [AutoAdd] Fix the logging for a failed added torrent
Add a missing arg to the failed added torrent callback and update the
logging text based on magnet or not.
2018-06-09 22:18:38 +01:00
cbdde7bba5 [Appveyor] Make win32 build files available as zip
It can be useful to have the build folder available so zip it up for
download as an artifact.

Although the 7z has an exclude for the installer exe it doesn't seem to
be working.
2018-06-05 23:21:12 +01:00
4fd51a4ef9 Skip Failing Tests On Windows 2018-06-02 22:09:50 +01:00
333c81c1d7 Add Appveyor support for Windows builds 2018-06-02 22:09:50 +01:00
21b5a15e5d Fix and cleanup outgoing interface code
There was a misunderstand about outgoing interface setting in libtorrent
and instead of being able to take both IP and adapater names, it only
accepts adapter names and errors with an IP address, which was the
default of '0.0.0.0' in code.

This fixes the code to not accept IP address and use empty string if it
is given one.

Also includes a bit of code cleanup.
2018-06-02 21:32:56 +01:00
edd431a304 Make Tox Multi-OS Friendly 2018-06-02 11:29:51 +01:00
d642fa3989 Fix files to pass new Flake8 checkers
Some new flake8 checkers were added so fix these new warnings and
any issues uncovered.

Use add-trailing-comma to fix missing trailing commas. It does not
format it as well as I would like however it was fast to change and
helps with git changes in future.

Removed pylint from tox due to large number of warnings.
2018-06-01 23:41:17 +01:00
bae1647e99 Add Pipenv Pipfile for development
Switching to Pipenv will speed up developement. See the docs for
details on using Pipenv.

 - Added more flake8 checks.
 - Added `detox` for running tests in parallel locally.
2018-06-01 23:41:17 +01:00
decd7aca71 Update gettext for outgoing interface 2018-06-01 12:57:18 +01:00
7cc9aaca49 Fix VersionSplit comparison
The tests on Python revealed a bug with comparing dev versions.

Switch to comparing by integers and setting non-dev version to infinity.

There is still an issue with suffix release comparisons beyond single
digits but will leave that for now.
2018-06-01 08:59:24 +01:00
196aa48727 [#3171] Add Option To Specify Outgoing Connection Interface 2018-05-21 13:14:15 -04:00
af2972f697 [GTKUI] Update prefs dialog ui file with Glade
Let Glade UI designer remove and make changes to UI file.
2018-05-21 08:30:25 +01:00
d4addeedd6 [UI] Fix non-unique hostlist host_id
Use a sha1 of time.time() can result in identical host_id. This was
evident with Travis tests randomly failing due to host_id collision
returning the wrong host details.

Using uuid4 to generate a random UUID in hex form should fix this issue.
2018-05-20 22:49:08 +01:00
8439698336 [Tox] Use platform independant toxworkdir instead of PWD
The use of `{env:PWD}` is not available on Windows so switch to
`{toxworkdir}` which is the directory where virtual environments
are created and sub directories for packaging reside.
2018-05-20 08:46:40 +01:00
7d120690ab [Docs] Fix Sphinx AutoDoc failing with gdk mask operation
The creation of a new mask from two gdk mask is causing a TypeError,
likely related to the mocking of gdk in Sphinx conf.
2018-05-20 08:46:25 +01:00
ee196f5035 [Flake8] Fix import and docstrings issues 2018-05-20 08:45:59 +01:00
ff85c334c7 [Tests] Fix 'Too many open files' by disabling LSD
Instances of libtorrent with Local Service Discovery enabled are leaving
many sockets fd open with every test run and will fail with 'Too many files
open' if ulimit is >=1024.
2018-05-19 21:50:23 +01:00
0c574f33e1 [Tests] Ensure tear_down deletes rpcserver and core 2018-05-19 21:23:46 +01:00
a7c7309027 Use context manager for open in metafile 2018-05-19 21:03:36 +01:00
de2f998218 [WebUI] Encode HTML entitiies
Ensure that torrent keys that could contain HTML entities are encoded
when displayed in webui.
2018-02-04 22:02:18 +00:00
4982ba0b98 Cleanup pytest config in tox
Config values are either wrong, unused or default is better.

Disable logging of deluge log output in testing.

Newer versions of pytest now use `pytest` cmd so rename.
2017-12-17 11:55:09 +00:00
f57286fd51 [#3121] Fix the peer-id to be unique
The associated ticket made us aware that the id '-DE2000-' might be
blocked.
2017-12-17 11:55:09 +00:00
12f7345d0c Use a constant for versions 2017-12-17 11:47:08 +00:00
4e79ed8124 Fix tox config to install latest pip packages
Trial changed command in latest versions of twisted
2017-12-16 17:39:48 +00:00
7787aa975f Delay assert in test_torrent for Travis 2017-12-16 17:29:30 +00:00
c13622a1e6 Fix state not loading after async API change 2017-12-16 13:21:47 +00:00
07a87fa15a [#3129|Console] Fix unable to use connect command from terminal
The parsed_cmd passed to do_command was an argparse Namespace object
which needed no further parsing so use exec_command instead.
2017-11-18 23:46:35 +00:00
2644169376 [#3126|Core] Restore synchonous add torrent methods for backward compatibilty
The synchonous add torrent method was replaced with async but this
break backward compatibility with 3rd party plugins and clients.

Added a new add_torrent_file_async method for adding single torrent.

Torrent manager has a new add_async method and split up code to prevent
duplication.

Update any use of add_torrent_file to add_torrent_file_async. Future
refactoring could use add_torrent_files instead.
2017-11-18 23:04:38 +00:00
5988f5f04f Fix flake8 error 2017-11-18 22:14:04 +00:00
95d826b77c [#3127|Blocklist] Fix importing blocklist with encoded lines
There are some blocklists with encoded names that break upon importing
so decode lines to unicode.

Need to use decode_bytes as not all encoded lines are utf8!
2017-11-05 20:52:51 +00:00
9bcda41700 [#3075|Console] Fix config handling windows paths
The console config token parser was unable to handle windows paths
starting with 'C:\'.

Remove unneeded windows_checks.
2017-11-05 17:30:47 +00:00
507c5df984 [#3112|Console] Fix handling hex for setting peer_tos in config
The token parser was converting hex value to int which is not what
should be passed onto libtorrent peer_tos setting.
2017-10-29 22:16:13 +00:00
0ba87b424c Revert "[#2848|Core] Fix incorrect share ratio in torrent status"
This reverts commit 7b87a93862.

After further discussion in the ticket this change is undesired.

> I wrongly assumed that private trackers will count how many bytes you
> download but they don't, they track how many parts(or chunks?) you
> have, when you announce it. So using total_done is fine, no change
> needed.

> Checking for private flag and using total_wanted_done for public
> enables users to be a bit more selfish. They can start a torrent,
> let it run for a bit then deselect the files they don't want and
> only upload enough to make up for what they wanted to download. This
> means they may upload less than they downloaded, hurting the swarm.
> So I personally don't think this would be a good change.
> Overall my suggestion is to close this as not a bug.
2017-10-29 12:36:15 +00:00
53f818e176 [#3070] Fix httpdownloader error with missing content-disposition filename
The parsing of the content-disposition in httpdownloader was not able to
handle missing parameters e.g. "Content-Disposition: attachment" and would
result in an IndexError. Added a test for this use-case.

Fixed the issue using the cgi.parse_header to extract the parameters.
2017-10-29 12:32:32 +00:00
00dcd60d56 [Lint] Fix flake8 issues with l as var 2017-10-29 11:39:52 +00:00
1730230244 [#3124|GTKUI] Fix comparing Name str if value is None
The original fix was not correct as the strcoll function cannot
accept None only strings. This fix ensures that the value is an
empty string if None for comparison.
2017-10-29 11:16:13 +00:00
0728c03c1c [#3066|Core] Add rather than replace dht bootstrap nodes in lt 2017-10-29 10:36:45 +00:00
354372b2ea [Notifications] Remove duplicate heading on prefs page
The heading is provided by prefs add_page so remove from glade file.
2017-10-16 21:57:08 +01:00
d169aca8bd [Notifications]Fix no text in tab list
The tab in the Preferences window is created and clickable - only the
text is missing.
2017-10-16 14:15:17 +03:00
26720ca4c2 [AutoAdd] Fix handling deferred torrents
* The changes to core api now return a deferred instead of torrent_id
   so need to update autoadd to use callbacks.
 * Minor refactor to save one indentation level and reuse fail callback.
 * Add exception handler for any errors from label plugin.
2017-10-14 21:39:30 +01:00
510a8b50b2 [AutoAdd] Update gtkui from libglade to gtkbuilder 2017-10-14 21:30:45 +01:00
d190f149d1 [WebUi] Update gtkui from libglade to gtkbuilder 2017-10-14 20:13:53 +01:00
24a31b1194 [Notifications] Update gtkui from libglade to gtkbuilder 2017-10-14 20:13:53 +01:00
470490769f [Extractor] Update gtkui from libglade to gtkbuilder 2017-10-14 20:13:53 +01:00
1259eca8ad [Execute] Update gtkui from libglade to gtkbuilder 2017-10-14 20:13:53 +01:00
f0316d3e31 [Blocklist] Update gtkui from libglade to gtkbuilder 2017-10-14 20:13:53 +01:00
9b580a87fa [GKTUI] Fix high priority files tab trigger 2017-10-14 20:06:14 +01:00
4bee1ce811 [Travis] Use current Trusty image
The issue with virtualenv site-packages was fixed: travis-ci/travis-cookbooks#878
2017-10-14 20:00:14 +01:00
e3f537770f [UI] Fix setting gettext to unicode for Py2 compat 2017-07-05 12:18:35 +01:00
9b92bc2baf [#3076|UI] Add ngettext and alias _n for plural translations
* Fixes use of _n() in console rm command
2017-07-05 09:46:12 +01:00
51b99caf24 [Common] Add decode_string (deprecated) for compatibility 2017-06-29 15:28:52 +01:00
850fd34522 [#3084] Fix error changing ownership on torrents 2017-06-29 15:07:11 +01:00
9164dafe69 [#3083] Fix missing common.utf8_encoded for backward compatibility 2017-06-29 14:42:16 +01:00
33e9545cd4 [#3079] Fix config parsing for json objects
* If a curly brace was used in a string then find_json_ojects would
   fail to find objects correctly. To fix this ignore double-quoted entries.
2017-06-28 10:32:45 +01:00
7b87a93862 [#2848|Core] Fix incorrect share ratio in torrent status
* Use total_wanted_done to increase the accuracy of the calculated ratio.
2017-06-27 19:02:15 +01:00
51bde704b5 [Packaging] Simplify release script using sdist
* setup.py sdist now creates a pristine tar which can be used for release.
 * Uses the version currently checked-out in git.
 * Removed unneeded lines in manifest.
2017-06-27 18:12:51 +01:00
3f13c24362 Update MANIFEST for wanted/unwanted files 2017-06-27 15:12:13 +01:00
6837d83f5b Fix setup.py requiring gen_web_gettext 2017-06-27 15:11:19 +01:00
3c1d7da698 [Packaging] Apply fixes to OSX app scripts
* Fix path to dist dir
 * Rename dylib with new soversion for lt 1.1.x
 * Create Info.plist with version and year automatically
2017-06-27 14:00:06 +01:00
d6731b8cee [#3078|GTKUI] Apply workaround for showing the OSX menu
Commenting out the remove_accelerator fixes showing the menubar correctly.
2017-06-27 09:16:51 +01:00
fe80703f95 [Packaging] Fix py2app build 2017-06-27 09:16:51 +01:00
1808ac506a [GTKUI] Restore removed os_check for systray icon
status_icon_new_from_icon_name does not work on OSX
2017-06-27 09:16:51 +01:00
3174c7534d [Core] Ensure tracker error message is decoded 2017-06-27 09:16:51 +01:00
065729a389 [OSX] Fix converting mac_ver to string 2017-06-27 09:16:51 +01:00
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
1305 changed files with 238476 additions and 232614 deletions

22
.gitattributes vendored
View File

@ -1,25 +1,5 @@
/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
*.py diff=python
ext-all.js diff=minjs

14
.gitignore vendored
View File

@ -1,14 +1,24 @@
*~
build
.cache
dist
*egg-info
docs/source/modules/deluge*.rst
*.egg-info/
*.dist-info/
*.egg
*.log
*.pyc
__pycache__/
*.py[cod]
*.tar.*
_trial_temp
.tox/
deluge/i18n/*/
deluge.pot
deluge/ui/web/js/*.js
deluge/ui/web/js/extjs/ext-extensions*.js
*.desktop
*.appdata.xml
.build_data*
osx/app
RELEASE-VERSION
.venv*

41
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,41 @@
default_language_version:
python: python3
exclude: >
(?x)^(
deluge/ui/web/docs/template/.*|
)$
repos:
- repo: https://github.com/ambv/black
rev: 19.3b0
hooks:
- id: black
name: Fmt Black
language_version: python3.6
- repo: https://github.com/prettier/prettier
rev: 1.17.0
hooks:
- id: prettier
name: Fmt Prettier
# Workaround to list modified files only.
args: [--list-different]
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.7
hooks:
- id: flake8
name: Chk Flake8
additional_dependencies:
- flake8-isort==2.7
- pep8-naming==0.8.2
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.2.1
hooks:
- id: double-quote-string-fixer
name: Fix Double-quotes
- id: end-of-file-fixer
name: Fix End-of-files
exclude_types: [javascript, css]
- id: mixed-line-ending
name: Fix Line endings
args: [--fix=auto]
- id: trailing-whitespace
name: Fix Trailing whitespace

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
deluge/ui/web/css/ext-*.css
deluge/ui/web/js/extjs/ext-*.js
deluge/ui/web/docs/
deluge/ui/web/themes/images/
*.py*
*.html

13
.prettierrc.yaml Normal file
View File

@ -0,0 +1,13 @@
trailingComma: "es5"
tabWidth: 4
singleQuote: true
overrides:
- files:
- "*.yaml"
- ".*.yaml"
- "*.yml"
- ".*.yml"
- "*.md"
options:
tabWidth: 2
singleQuote: false

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, bad-continuation,
# 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

22
.readthedocs.yml Normal file
View File

@ -0,0 +1,22 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: requirements.txt
- requirements: docs/requirements.txt
- method: setuptools
path: .

83
.travis.yml Normal file
View File

@ -0,0 +1,83 @@
dist: xenial
sudo: required
language: python
python:
# Travis Xenial Python to support system_site_packages
- 3.5
cache: pip
virtualenv:
system_site_packages: true
env:
global:
- DISPLAY=:99.0
git:
# Set greater depth to get version from tags.
depth: 1000
matrix:
include:
- name: Unit tests
env: TOX_ENV=py3
- name: Unit tests - libtorrent 1.2
env: TOX_ENV=py3
addons:
apt:
sources: [sourceline: "ppa:libtorrent.org/1.2-daily"]
packages: [python3-libtorrent, python3-venv]
- name: Unit tests - Python 2
env: TOX_ENV=py27
python: 2.7
- if: commit_message =~ SECURITY_TEST
env: TOX_ENV=security
- name: Code linting
env: TOX_ENV=lint
- name: Docs build
env: TOX_ENV=docs
- name: GTK unit tests
env: TOX_ENV=gtkui
- name: Plugins unit tests
env: TOX_ENV=plugins
addons:
apt:
sources:
- sourceline: "ppa:libtorrent.org/rc-1.1-daily"
- deadsnakes
packages:
- python-libtorrent
- python3-libtorrent
# Install py36 specifically for pre-commit to run black formatter.
- python3.6
# Intall python3-venv to provide ensurepip module for tox.
- python3-venv
# Install dependencies
install:
- pip install tox tox-venv
# GTKUI tests
- "if [ $TOX_ENV == 'gtkui' ]; then
sudo apt install python3-gi python3-gi-cairo gir1.2-gtk-3.0;
fi"
# Security tests
- "if [ $TOX_ENV == 'security' ]; then
testssl_url=https://github.com/drwetter/testssl.sh/archive/v2.9.5-5.tar.gz;
wget -O- $testssl_url | tar xz
&& mv -t deluge/tests/data testssl.sh-2.9.5-5/testssl.sh testssl.sh-2.9.5-5/etc/;
fi"
before_script:
- export PYTHONPATH=$PYTHONPATH:$PWD
# Verify libtorrent installed and version
- python -c "import libtorrent as lt; print(lt.__version__)"
# Start xvfb for the GTKUI tests
- "if [ $TOX_ENV == 'gtkui' ]; then
/sbin/start-stop-daemon --start --quiet --background \
--make-pidfile --pidfile /tmp/custom_xvfb_99.pid \
--exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16;
fi"
script:
- 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

100
CHANGELOG.md Normal file
View File

@ -0,0 +1,100 @@
# Changelog
## 2.0.3 (2019-06-12)
### Gtk UI
- Fix errors running on Wayland (#3265).
- Fix Peers Tab tooltip and context menu errors (#3266).
### Web UI
- Fix TypeError in Peers Tab setting country flag.
- Fix reverse proxy header TypeError (#3260).
- Fix request.base 'idna' codec error (#3261).
- Fix unable to change password (#3262).
### Extractor plugin
- Fix potential error starting plugin.
### Documentation
- Fix macOS install typo.
- Fix Windows install instructions.
## 2.0.2 (2019-06-08)
### Packaging
- Add systemd deluged and deluge-web service files to package tarball (#2034)
### Core
- Fix Python 2 compatiblity issue with SimpleNamespace.
## 2.0.1 (2019-06-07)
### Packaging
- Fix setup.py build error without git installed.
## 2.0.0 (2019-06-06)
### Codebase
- Ported to Python 3
### Core
- 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.
- Updated SSL/TLS Protocol parameters for better security.
- 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
- Add Option To Specify Outgoing Connection Interface.
- Fix potential for host_id collision when creating hostlist entries.
### Gtk UI
- Ported to GTK3 (3rd-party plugins will need updated).
- 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
### Web UI
- 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
- 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.

521
ChangeLog
View File

@ -1,521 +0,0 @@
=== 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.
==== 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
==== 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.
==== 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".

30
DEPENDS
View File

@ -1,30 +0,0 @@
=== Core ===
* python >= 2.6
* twisted >= 8.1
* twisted-web >= 8.1
* pyopenssl
* setuptools
* gettext
* intltool
* pyxdg
* chardet
* geoip-database (optional)
* setproctitle (optional)
* rencode >= 1.0.2 (optional), a Python port is already included
* libtorrent (rasterbar) >= 0.16.7
* If building libtorrent:
* boost >= 1.40
* openssl
* zlib
=== Gtk ===
* pygtk >= 2.16
* librsvg
* xdg-utils
* python-notify (optional)
* pygame (optional)
=== Web ===
* mako

101
DEPENDS.md Normal file
View File

@ -0,0 +1,101 @@
# Deluge dependencies
The following are required to install and run Deluge. They are separated into
sections to distinguish the precise requirements for each module.
All modules will require the [common](#common) section dependencies.
## Prerequisite
- [Python] _>= 3.5_
## Build
- [setuptools]
- [intltool] - Optional: Desktop file translation for \*nix.
- [closure-compiler] - Minify javascript (alternative is [slimit])
## Common
- [Twisted] _>= 17.1_ - Use `TLS` extras for `service_identity` and `idna`.
- [OpenSSL] _>= 1.0.1_
- [pyOpenSSL]
- [rencode] _>= 1.0.2_ - Encoding library.
- [PyXDG] - Access freedesktop.org standards for \*nix.
- [xdg-utils] - Provides xdg-open for \*nix.
- [six]
- [zope.interface]
- [chardet] - Optional: Encoding detection.
- [setproctitle] - Optional: Renaming processes.
- [Pillow] - Optional: Support for resizing tracker icons.
- [dbus-python] - Optional: Show item location in filemanager.
#### Linux and BSD
- [distro] - Optional: OS platform information.
#### Windows OS
- [pywin32]
- [certifi]
## Core (deluged daemon)
- [libtorrent] _>= 1.1.1_
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_)
## GTK UI
- [GTK+] >= 3.10
- [PyGObject]
- [Pycairo]
- [librsvg] _>= 2_
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
#### MacOS
- [GtkOSXApplication]
## Web UI
- [mako]
## Plugins
### Notifications
- [pygame] - Optional: Play sounds
- [libnotify] w/GIR - Optional: Desktop popups.
[python]: https://www.python.org/
[setuptools]: https://setuptools.readthedocs.io/en/latest/
[intltool]: https://freedesktop.org/wiki/Software/intltool/
[closure-compiler]: https://developers.google.com/closure/compiler/
[slimit]: https://slimit.readthedocs.io/en/latest/
[openssl]: https://www.openssl.org/
[pyopenssl]: https://pyopenssl.org
[twisted]: https://twistedmatrix.com
[pillow]: https://pypi.org/project/Pillow/
[libtorrent]: https://libtorrent.org/
[zope.interface]: https://pypi.org/project/zope.interface/
[distro]: https://github.com/nir0s/distro
[pywin32]: https://github.com/mhammond/pywin32
[certifi]: https://pypi.org/project/certifi/
[py2-ipaddress]: https://pypi.org/project/py2-ipaddress/
[dbus-python]: https://pypi.org/project/dbus-python/
[setproctitle]: https://pypi.org/project/setproctitle/
[gtkosxapplication]: https://github.com/jralls/gtk-mac-integration
[chardet]: https://chardet.github.io/
[rencode]: https://github.com/aresch/rencode
[pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/
[six]: https://pythonhosted.org/six/
[xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/
[gtk+]: https://www.gtk.org/
[pycairo]: https://cairographics.org/pycairo/
[pygobject]: https://pygobject.readthedocs.io/en/latest/
[geoip]: https://pypi.org/project/GeoIP/
[mako]: https://www.makotemplates.org/
[pygame]: https://www.pygame.org/
[libnotify]: https://developer.gnome.org/libnotify/
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg

View File

@ -1,22 +1,35 @@
include AUTHORS ChangeLog DEPENDS ez_setup.py LICENSE msgfmt.py RELEASE-VERSION version.py
include *.md
include AUTHORS
include LICENSE
include RELEASE-VERSION
include msgfmt.py
include minify_web_js.py
include version.py
include gen_web_gettext.py
graft docs/man
graft packaging/systemd
include deluge/i18n/*.po
recursive-exclude deluge/i18n *.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/
recursive-exclude deluge/tests *.pyc
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

57
README
View File

@ -1,57 +0,0 @@
==========================
Deluge BitTorrent Client
==========================
Homepage: http://deluge-torrent.org
Authors:
Andrew Resch
Damien Churchill
For contributors and past developers see:
AUTHORS
==========================
Installation Instructions:
==========================
For detailed instructions see: http://dev.deluge-torrent.org/wiki/Installing/Source
Ensure build dependencies are installed, see DEPENDS for a full listing.
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
==========================
FAQ
==========================
For the full FAQ see: http://dev.deluge-torrent.org/wiki/Faq
How to start the various user-interfaces
Gtk:
deluge or deluge-gtk
Console:
deluge-console
Web:
deluge-web
Go to http://localhost:8112/ default-password = "deluge"
How do I start the daemon?
deluged
I can't connect to the daemon from another machine
See: http://dev.deluge-torrent.org/wiki/UserGuide/ThinClient

63
README.md Normal file
View File

@ -0,0 +1,63 @@
# Deluge BitTorrent Client
[![build-status]][travis-deluge] [![docs-status]][rtd-deluge]
Deluge is a BitTorrent client that utilizes a daemon/client model.
It has various user interfaces available such as the GTK-UI, Web-UI and
a Console-UI. It uses [libtorrent][lt] at it's core to handle the BitTorrent
protocol.
## Install
From [PyPi](https://pypi.org/project/deluge):
pip install deluge
From source code:
python setup.py build
python setup.py install
See [DEPENDS](DEPENDS.md) and [Installing/Source] for dependency details.
## Usage
The various user-interfaces and Deluge daemon can be started with the following commands.
Use the `--help` option for further command options.
### Gtk UI
`deluge` or `deluge-gtk`
### Console UI
`deluge-console`
### Web UI
`deluge-web`
Open http://localhost:8112 with default password `deluge`.
### Daemon
`deluged`
See the [Thinclient guide] to connect to the daemon from another computer.
## Contact
- [Homepage](https://deluge-torrent.org)
- [User guide][user guide]
- [Forum](https://forum.deluge-torrent.org)
- [IRC Freenode #deluge](irc://irc.freenode.net/deluge)
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient
[installing/source]: https://dev.deluge-torrent.org/wiki/Installing/Source
[build-status]: https://travis-ci.org/deluge-torrent/deluge.svg "Travis Status"
[travis-deluge]: https://travis-ci.org/deluge-torrent/deluge
[docs-status]: https://readthedocs.org/projects/deluge/badge/?version=develop
[rtd-deluge]: https://deluge.readthedocs.io/en/develop/?badge=develop "Documentation Status"
[lt]: https://libtorrent.org

53
appveyor.yml Normal file
View File

@ -0,0 +1,53 @@
environment:
PYTHON_VERSION: 3.6
PYTHON_ARCH: 64
PYTHON: "C:\\Python36-x64"
APPVEYOR_SAVE_CACHE_ON_ERROR: true
matrix:
- TOXENV: py36
pull_requests:
do_not_increment_build_number: true
install:
# If there is a newer build queued for same PR, cancel this one. Credit: JuliaLang devs
- ps:
if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
throw "There are newer queued builds for this pull request, failing early." }
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
- "python -VV"
- if defined TOXENV (
python -m pip install tox tox_venv
) else (
python -m pip install -rrequirements.txt pygame bbfreeze pefile
)
- "SET PATH=C:\\OpenSSL-v11-Win64\\bin;%PATH%"
- openssl version -v
- python -m pip install deluge-libtorrent
- 'python -c "import libtorrent; print(libtorrent.__version__)"'
cache:
- '%LOCALAPPDATA%\pip\cache'
build: false
test_script:
- if defined TOXENV tox
# Commented out as require GTK3 to create package.
# after_test:
# - if not defined TOXENV python setup.py build && python setup.py install
# - cd %APPVEYOR_BUILD_FOLDER%\\packaging\\win32
# - if not defined TOXENV deluge-bbfreeze.py debug
# - "SET PATH=C:\\Program Files (x86)\\NSIS;%PATH%"
# - if not defined TOXENV makensis deluge-win32-installer.nsi
# - if not defined TOXENV 7z a deluge-win32.zip build-win32 "-x!*.exe"
# artifacts:
# - path: packaging\win32\deluge-win32.zip
# - path: packaging\win32\build-win32\*.exe
#on_success:
#

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 @@
"""Deluge"""
# this is a namespace package
import pkg_resources
pkg_resources.declare_namespace(__name__)

View File

@ -1,20 +0,0 @@
from new import classobj
from deluge.core.core import Core
from deluge.core.daemon import Daemon
class RpcApi:
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'):
continue
methods[d] = getattr(obj, d)
cobj = classobj(obj.__name__.lower(), (object,), methods)
setattr(RpcApi, obj.__name__.lower(), cobj)
scan_for_methods(Core)
scan_for_methods(Daemon)

View File

@ -1,60 +1,33 @@
#
# _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'
LT_VERSION = lt.__version__
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
raise ImportError(
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
)

387
deluge/argparserbase.py Normal file
View File

@ -0,0 +1,387 @@
# -*- coding: utf-8 -*-
#
# 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.
#
from __future__ import unicode_literals
import argparse
import logging
import os
import platform
import sys
import textwrap
import deluge.log
from deluge import common
from deluge.configmanager import get_config_dir, set_config_dir
def find_subcommand(self, args=None, sys_argv=True):
"""Find if a subcommand has been supplied.
Args:
args (list, optional): The argument list to search through.
sys_argv (bool): Use sys.argv[1:] if args is None.
Returns:
int: Index of the subcommand or '-1' if none found.
"""
subcommand_found = -1
if args is None:
args = sys.argv[1:] if sys_argv is None else []
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map:
if sp_name in args:
subcommand_found = args.index(sp_name)
return subcommand_found
def set_default_subparser(self, name, abort_opts=None):
"""Sets the default argparse subparser.
Args:
name (str): The name of the default subparser.
abort_opts (list): The arguments to test for in case no subcommand is found.
If any of the values are found, the default subparser will
not be inserted into sys.argv.
Returns:
list: The arguments found in sys.argv if no subcommand found, else None
"""
found_abort_opts = []
abort_opts = [] if abort_opts is None else abort_opts
test_args = sys.argv[1:]
subparser_found = self.find_subcommand(args=test_args)
for i, arg in enumerate(test_args):
if subparser_found == i:
break
if arg in abort_opts:
found_abort_opts.append(arg)
if subparser_found == -1:
if found_abort_opts:
# Found one or more of arguments in abort_opts
return found_abort_opts
# insert default in first position, this implies no
# global options without a sub_parsers specified
sys.argv.insert(1, name)
return None
argparse.ArgumentParser.find_subcommand = find_subcommand
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def _get_version_detail():
version_str = '%s\n' % (common.get_version())
try:
from deluge._libtorrent import LT_VERSION
version_str += 'libtorrent: %s\n' % LT_VERSION
except ImportError:
pass
version_str += 'Python: %s\n' % platform.python_version()
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
return version_str
class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
"""Help message formatter which retains formatting of all help text."""
def _split_lines(self, text, width):
"""
Do not remove whitespaces in string but still wrap text to max width.
Instead of passing the entire text to textwrap.wrap, split and pass each
line instead. This way list formatting is not mangled by textwrap.wrap.
"""
wrapped_lines = []
for l in text.splitlines():
wrapped_lines.extend(textwrap.wrap(l, width, subsequent_indent=' '))
return wrapped_lines
def _format_action_invocation(self, action):
"""
Combines the options with comma and displays the argument
value only once instead of after both options.
Instead of: -s <arg>, --long-opt <arg>
Show : -s, --long-opt <arg>
"""
if not action.option_strings:
metavar, = self._metavar_formatter(action, action.dest)(1)
return metavar
else:
parts = []
# if the Optional doesn't take a value, format is:
# -s, --long
if action.nargs == 0:
parts.extend(action.option_strings)
# if the Optional takes a value, format is:
# -s, --long ARGS
else:
default = action.dest.upper()
args_string = self._format_args(action, default)
opt = ', '.join(action.option_strings)
parts.append('%s %s' % (opt, args_string))
return ', '.join(parts)
class HelpAction(argparse._HelpAction):
def __call__(self, parser, namespace, values, option_string=None):
if hasattr(parser, 'subparser'):
subparser = getattr(parser, 'subparser')
subparser.print_help()
else:
parser.print_help()
parser.exit()
class ArgParserBase(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
if 'formatter_class' not in kwargs:
kwargs['formatter_class'] = lambda prog: DelugeTextHelpFormatter(
prog, max_help_position=33, width=90
)
kwargs['add_help'] = kwargs.get('add_help', False)
common_help = kwargs.pop('common_help', True)
self.log_stream = sys.stdout
if 'log_stream' in kwargs:
self.log_stream = kwargs['log_stream']
del kwargs['log_stream']
super(ArgParserBase, self).__init__(*args, **kwargs)
self.common_setup = False
self.process_arg_group = False
self.group = self.add_argument_group(_('Common Options'))
if common_help:
self.group.add_argument(
'-h', '--help', action=HelpAction, help=_('Print this help message')
)
self.group.add_argument(
'-V',
'--version',
action='version',
version='%(prog)s ' + _get_version_detail(),
help=_('Print version information'),
)
self.group.add_argument(
'-v',
action='version',
version='%(prog)s ' + _get_version_detail(),
help=argparse.SUPPRESS,
) # Deprecated arg
self.group.add_argument(
'-c',
'--config',
metavar='<config>',
help=_('Set the config directory path'),
)
self.group.add_argument(
'-l',
'--logfile',
metavar='<logfile>',
help=_('Output to specified logfile instead of stdout'),
)
self.group.add_argument(
'-L',
'--loglevel',
choices=[l for k in deluge.log.levels for l in (k, k.upper())],
help=_('Set the log level (none, error, warning, info, debug)'),
metavar='<level>',
)
self.group.add_argument(
'--logrotate',
nargs='?',
const='2M',
metavar='<max-size>',
help=_(
'Enable logfile rotation, with optional maximum logfile size, '
'default: %(const)s (Logfile rotation count is 5)'
),
)
self.group.add_argument(
'-q',
'--quiet',
action='store_true',
help=_('Quieten logging output (Same as `--loglevel none`)'),
)
self.group.add_argument(
'--profile',
metavar='<profile-file>',
nargs='?',
default=False,
help=_(
'Profile %(prog)s with cProfile. Outputs to stdout '
'unless a filename is specified'
),
)
def parse_args(self, args=None):
"""Parse UI arguments and handle common and process group options.
Notes:
Unknown arguments results in usage text printed and system exit.
Args:
args (list, optional): The arguments to parse.
Returns:
argparse.Namespace: The parsed arguments.
"""
options = super(ArgParserBase, self).parse_args(args=args)
return self._handle_ui_options(options)
def parse_known_ui_args(self, args, withhold=None):
"""Parse UI arguments and handle common and process group options without error.
Args:
args (list): The arguments to parse.
withhold (list): Values to ignore in the args list.
Returns:
argparse.Namespace: The parsed arguments.
"""
if withhold:
args = [a for a in args if a not in withhold]
options, remaining = super(ArgParserBase, self).parse_known_args(args=args)
options.remaining = remaining
# Hanlde common and process group options
return self._handle_ui_options(options)
def _handle_ui_options(self, options):
"""Handle UI common and process group options.
Args:
options (argparse.Namespace): The parsed options.
Returns:
argparse.Namespace: The parsed options.
"""
if not self.common_setup:
self.common_setup = True
# Setup the logger
if options.quiet:
options.loglevel = 'none'
if options.loglevel:
options.loglevel = options.loglevel.lower()
logfile_mode = 'w'
logrotate = options.logrotate
if options.logrotate:
logfile_mode = 'a'
logrotate = common.parse_human_size(options.logrotate)
# Setup the logger
deluge.log.setup_logger(
level=options.loglevel,
filename=options.logfile,
filemode=logfile_mode,
logrotate=logrotate,
output_stream=self.log_stream,
)
if options.config:
if not set_config_dir(options.config):
log = logging.getLogger(__name__)
log.error('There was an error setting the config dir! Exiting..')
sys.exit(1)
else:
if not os.path.exists(common.get_default_config_dir()):
os.makedirs(common.get_default_config_dir())
if self.process_arg_group:
self.process_arg_group = False
# If donotdaemonize is set, skip process forking.
if not (common.windows_check() or options.donotdaemonize):
if os.fork():
os._exit(0)
os.setsid()
# Do second fork
if os.fork():
os._exit(0)
# Ensure process doesn't keep any directory in use that may prevent a filesystem unmount.
os.chdir(get_config_dir())
# Write pid file before chuid
if options.pidfile:
with open(options.pidfile, 'wb') as _file:
_file.write('%d\n' % os.getpid())
if not 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)
return options
def add_process_arg_group(self):
"""Adds a grouping of common process args to control a daemon to the parser"""
self.process_arg_group = True
self.group = self.add_argument_group(_('Process Control Options'))
self.group.add_argument(
'-P',
'--pidfile',
metavar='<pidfile>',
action='store',
help=_('Pidfile to store the process id'),
)
if not common.windows_check():
self.group.add_argument(
'-d',
'--do-not-daemonize',
dest='donotdaemonize',
action='store_true',
help=_('Do not daemonize (fork) this process'),
)
self.group.add_argument(
'-f',
'--fork',
dest='donotdaemonize',
action='store_false',
help=argparse.SUPPRESS,
) # Deprecated arg
self.group.add_argument(
'-U',
'--user',
metavar='<user>',
action='store',
help=_('Change to this user on startup (Requires root)'),
)
self.group.add_argument(
'-g',
'--group',
metavar='<group>',
action='store',
help=_('Change to this group on startup (Requires root)'),
)

View File

@ -9,66 +9,85 @@
# License.
# Written by Petru Paler
# Updated by Calum Lind to support both Python 2 and Python 3.
from __future__ import unicode_literals
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)
except (IndexError, KeyError, ValueError):
raise Exception("not a valid bencoded string")
return r
from types import StringType, IntType, LongType, DictType, ListType, TupleType
r, __ = decode_func[x[0:1]](x, 0)
except (LookupError, TypeError, ValueError):
raise BTFailure('Not a valid bencoded string')
else:
return r
class Bencached(object):
@ -78,52 +97,62 @@ 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_bytes(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()):
try:
k = k.encode('utf8')
except AttributeError:
pass
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 # noqa: F821
encode_func[str] = encode_bytes
encode_func[unicode] = encode_string # noqa: F821
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,53 @@
#
# 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 six import string_types
from twisted.internet import reactor
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
from twisted.internet.task import LoopingCall, deferLater
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`.
@ -89,11 +91,20 @@ class Component(object):
still be considered in a Started state.
"""
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 +115,62 @@ 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 +178,55 @@ 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 +234,9 @@ class Component(object):
d.addCallback(on_stop)
return d
def get_state(self):
return self._component_state
def start(self):
pass
@ -218,32 +249,36 @@ 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 = {}
# Stores all of the components that are dependent on a particular component
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)
'Component already registered with name %s' % name
)
self.components[obj._component_name] = obj
if obj._component_depend:
@ -251,42 +286,45 @@ 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, string_types):
names = [names]
def on_depends_started(result, name):
@ -305,22 +343,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, string_types):
names = [names]
def on_dependents_stopped(result, name):
@ -335,7 +373,9 @@ class ComponentRegistry(object):
if name in self.components:
if name in self.dependents:
# If other components depend on this component, stop them first
d = self.stop(self.dependents[name]).addCallback(on_dependents_stopped, name)
d = self.stop(self.dependents[name]).addCallback(
on_dependents_stopped, name
)
deferreds.append(d)
stopped_in_deferred.update(self.dependents[name])
else:
@ -343,81 +383,84 @@ 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, string_types):
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, string_types):
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 +472,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,96 +39,111 @@ 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
from tempfile import NamedTemporaryFile
import deluge.common
import six.moves.cPickle as pickle # noqa: N813
json = deluge.common.json
from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__)
callLater = None # noqa: N816 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 []
quoted = False
for index, c in enumerate(s[offset:]):
if c == "{":
if c == '"':
quoted = not quoted
elif quoted:
continue
elif 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
}
self.__version = {'format': 1, 'file': file_version}
# This will get set with a reactor.callLater whenever a config option
# is set.
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 +151,136 @@ 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:
# Change the value type if it is not None and does not match.
type_match = isinstance(self.__config[key], (type(None), type(value)))
if value is not None and not type_match:
try:
if oldtype == unicode:
value = oldtype(value, "utf8")
else:
oldtype = type(self.__config[key])
# Don't convert to bytes as requires encoding and value will
# be decoded anyway.
if oldtype is not bytes:
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 = 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 +290,69 @@ 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 +364,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 +416,42 @@ 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 +459,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 +471,41 @@ 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)
with NamedTemporaryFile(
prefix=os.path.basename(filename) + '.', delete=False
) as _file:
filename_tmp = _file.name
log.debug('Saving new config file %s', filename_tmp)
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
# Resolve symlinked config files before backing up and saving.
filename = os.path.realpath(filename)
# 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_tmp, filename)
shutil.move(filename_tmp, filename)
except IOError as ex:
log.error('Error moving new config file: %s', ex)
return False
else:
return True
@ -480,36 +514,42 @@ 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 +557,13 @@ 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,42 @@ 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,78 @@
#
# 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
import types
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
try:
SimpleNamespace = types.SimpleNamespace # Python 3.3+
except AttributeError:
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 SimpleNamespace(object): # Python 2.7
def __init__(self, **attr):
self.__dict__.update(attr)
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 +91,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 +100,53 @@ 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)
# Copy alert attributes
alert_copy = SimpleNamespace(
**{
attr: getattr(alert, attr)
for attr in dir(alert)
if not attr.startswith('__')
}
)
self.delayed_calls.append(reactor.callLater(0, handler, alert_copy))
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,31 @@
#
# 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 +34,14 @@ 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
@ -74,17 +52,19 @@ class Account(object):
'username': self.username,
'password': self.password,
'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel],
'authlevel_int': self.authlevel
'authlevel_int': self.authlevel,
}
def __repr__(self):
return ('<Account username="%(username)s" authlevel=%(authlevel)s>' %
self.__dict__)
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
'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 +78,96 @@ 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.__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 +175,45 @@ 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 +224,33 @@ 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 +260,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 +271,19 @@ 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,205 @@
#
# 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,
outgoing_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.
outgoing_interface (str, optional): The network interface name or
IP address to open outgoing 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,
outgoing_interface=outgoing_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,
interface=interface
allow_remote=self.core.config['allow_remote'],
listen=not standalone,
interface=interface,
)
log.debug(
'Listening to UI on: %s:%s and bittorrent on: %s Making connections out on: %s',
interface,
port,
listen_interface,
outgoing_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()
def get_version(self):
"""Returns the daemon version"""
return get_version()
@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
)

143
deluge/core/daemon_entry.py Normal file
View File

@ -0,0 +1,143 @@
# -*- 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.argparserbase import ArgParserBase
from deluge.common import run_profiled
from deluge.configmanager import get_config_dir
from deluge.i18n import setup_mock_translation
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(
'-o',
'--outgoing-interface',
metavar='<interface>',
dest='outgoing_interface',
action='store',
help=_(
'The network interface name or IP address for outgoing 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
"""
setup_mock_translation()
# Setup the argument parser
parser = ArgParserBase()
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,
outgoing_interface=options.outgoing_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,20 @@ 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,66 @@
#
# 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"]
from six import string_types
import deluge.component as component
from deluge.common import 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 +80,53 @@ 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 +136,60 @@ 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, string_types):
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:
for field, values in filter_dict.items():
# Registered filters
for field, values in list(filter_dict.items()):
if field in self.registered_filters:
# a set filters out the doubles.
torrent_ids = list(set(self.registered_filters[field](torrent_ids, values)))
# 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,55 +199,58 @@ 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)
torrent_keys, plugin_keys = self.torrents.separate_keys(tree_keys, torrent_ids)
items = dict((field, self.tree_fields[field]()) for field in tree_keys)
items = {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(key=self._sort_state_item)
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,28 +259,23 @@ 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 list(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:
ix = 99
if y[0] in STATE_SORT:
iy = STATE_SORT.index(y[0])
else:
iy = 99
return ix - iy
def _sort_state_item(self, item):
try:
return STATE_SORT.index(item[0])
except ValueError:
return 99

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,40 @@
#
# 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 +48,44 @@ 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 +96,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,162 @@
#
# 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': '',
'outgoing_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 +169,131 @@ 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_outgoing_interface(self, key, value):
"""Set interface name or IP address for outgoing BitTorrent connections."""
value = value.strip() if value else ''
self.core.apply_session_settings({'outgoing_interfaces': value})
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']
if self.config['listen_interface']:
interface = self.config['listen_interface'].strip()
else:
interface = '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()
lt_bootstraps = self.core.session.get_settings()['dht_bootstrap_nodes']
# Update list of lt bootstraps, using set to remove duplicates.
dht_bootstraps = set(
lt_bootstraps.split(',')
+ [
'router.bittorrent.com:6881',
'router.utorrent.com:6881',
'router.bitcomet.com:6881',
'dht.transmissionbt.com:6881',
'dht.aelitis.com:6881',
]
)
self.core.apply_session_settings(
{'dht_bootstrap_nodes': ','.join(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 +304,179 @@ 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_t.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_t.i2p_proxy:
proxy_settings.update(
{
'proxy_type': lt.proxy_type_t.i2p_proxy,
'i2p_hostname': value['hostname'],
'i2p_port': value['port'],
}
)
elif value['type'] != lt.proxy_type_t.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,43 @@
#
# 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 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.crypto_utils import get_context_factory
from deluge.error import (
DelugeError,
IncompatibleClient,
NotAuthorizedError,
WrappedException,
_ClientSideRecreateError,
)
from deluge.event import ClientDisconnectedEvent
from deluge.transfer import DelugeTransferProtocol
RPC_RESPONSE = 1
@ -68,6 +46,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
@ -79,17 +58,27 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
:type auth_level: int
"""
def wrap(func, *args, **kwargs):
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
if doc:
func.__doc__ += doc
rpc_text = '**RPC exported method** (*Auth level: %s*)' % auth_level
# Append the RPC text while ensuring correct docstring formatting.
if func.__doc__:
if func.__doc__.endswith(' '):
indent = func.__doc__.split('\n')[-1]
func.__doc__ += '\n{}'.format(indent)
else:
func.__doc__ += '\n\n'
func.__doc__ += rpc_text
else:
func.__doc__ = rpc_text
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 +98,25 @@ 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):
"""
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"))
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 +124,30 @@ 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 +156,25 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
:type data: object
"""
self.transfer_message(data)
try:
self.transfer_message(data)
except Exception as ex:
log.warning('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",
peer.host, peer.port)
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 +190,11 @@ 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,60 +215,77 @@ 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))
exc_type, exc_value, dummy_exc_trace = sys.exc_info()
formated_tb = traceback.format_exc()
try:
self.sendData((
RPC_ERROR,
request_id,
exceptionType.__name__,
exceptionValue._args,
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)
self.sendData(
(
RPC_ERROR,
request_id,
exc_type.__name__,
exc_value._args,
exc_value._kwargs,
formated_tb,
)
)
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()
raise WrappedException(
str(exc_value), exc_type.__name__, formated_tb
)
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 +293,66 @@ 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 +370,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 +392,28 @@ 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()
cert = os.path.join(deluge.configmanager.get_config_dir('ssl'), 'daemon.cert')
pkey = os.path.join(deluge.configmanager.get_config_dir('ssl'), 'daemon.pkey')
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)
reactor.listenSSL(
port, self.factory, get_context_factory(cert, pkey), interface=hostname
)
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 +429,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 +443,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 +467,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 +488,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 +505,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 +514,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 +536,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 +556,72 @@ 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\".",
event.name, event.args, session_id)
self.factory.session_protocols[session_id].sendData((RPC_EVENT, event.name, event.args))
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 +629,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

79
deluge/crypto_utils.py Normal file
View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 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 __future__ import division, print_function, unicode_literals
from OpenSSL.crypto import FILETYPE_PEM
from twisted.internet.ssl import (
AcceptableCiphers,
Certificate,
CertificateOptions,
KeyPair,
TLSVersion,
)
# A TLS ciphers list.
# Sources for more information on TLS ciphers:
# - https://wiki.mozilla.org/Security/Server_Side_TLS
# - https://www.ssllabs.com/projects/best-practices/index.html
# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
#
# This list was inspired by the `urllib3` library
# - https://github.com/urllib3/urllib3/blob/master/urllib3/util/ssl_.py#L79
#
# The general intent is:
# - prefer cipher suites that offer perfect forward secrecy (ECDHE),
# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common,
# - disable NULL authentication, MD5 MACs and DSS for security reasons.
TLS_CIPHERS = ':'.join(
[
'ECDH+AESGCM',
'ECDH+CHACHA20',
'AES256-GCM-SHA384',
'AES128-GCM-SHA256',
'!DSS' '!aNULL',
'!eNULL',
'!MD5',
]
)
# This value tells OpenSSL to disable all SSL/TLS renegotiation.
SSL_OP_NO_RENEGOTIATION = 0x40000000
def get_context_factory(cert_path, pkey_path):
"""OpenSSL context factory.
Generates an OpenSSL context factory using Twisted's CertificateOptions class.
This will keep a server cipher order.
Args:
cert_path (string): The path to the certificate file
pkey_path (string): The path to the private key file
Returns:
twisted.internet.ssl.CertificateOptions: An OpenSSL context factory
"""
with open(cert_path) as cert:
certificate = Certificate.loadPEM(cert.read()).original
with open(pkey_path) as pkey:
private_key = KeyPair.load(pkey.read(), FILETYPE_PEM).original
ciphers = AcceptableCiphers.fromOpenSSLCipherString(TLS_CIPHERS)
cert_options = CertificateOptions(
privateKey=private_key,
certificate=certificate,
raiseMinimumTo=TLSVersion.TLSv1_2,
acceptableCiphers=ciphers,
)
ctx = cert_options.getContext()
ctx.use_certificate_chain_file(cert_path)
ctx.set_options(SSL_OP_NO_RENEGOTIATION)
return cert_options

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
@ -43,9 +23,142 @@ def proxy(proxy_func):
:param proxy_func: the proxy function
:type proxy_func: function
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
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,131 +1,96 @@
#
# 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)
inst._args = args
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
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'
) % {'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" %
dict(current_level=current_level, required_level=required_level)
)
msg = ('Auth level too low: %(current_level)s < %(required_level)s') % {
'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,19 +14,25 @@ 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
import six
known_events = {}
class DelugeEventMetaClass(type):
"""
This metaclass simply keeps a list of all events classes created.
"""
def __init__(cls, name, bases, dct):
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
if name != "DelugeEvent":
if name != 'DelugeEvent':
known_events[name] = cls
class DelugeEvent(object):
class DelugeEvent(six.with_metaclass(DelugeEventMetaClass, object)):
"""
The base class for all events.
@ -62,23 +42,24 @@ class DelugeEvent(object):
:type args: list
"""
__metaclass__ = DelugeEventMetaClass
def _get_name(self):
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.
"""
def __init__(self, torrent_id, from_state):
"""
:param torrent_id: the torrent_id of the torrent that was added
@ -88,10 +69,12 @@ class TorrentAddedEvent(DelugeEvent):
"""
self._args = [torrent_id, from_state]
class TorrentRemovedEvent(DelugeEvent):
"""
Emitted when a torrent has been removed from the session.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -99,10 +82,12 @@ class TorrentRemovedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class PreTorrentRemovedEvent(DelugeEvent):
"""
Emitted when a torrent is about to be removed from the session.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -110,10 +95,12 @@ class PreTorrentRemovedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentStateChangedEvent(DelugeEvent):
"""
Emitted when a torrent changes state.
"""
def __init__(self, torrent_id, state):
"""
:param torrent_id: the torrent_id
@ -123,16 +110,34 @@ 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.
"""
def __init__(self, torrent_id, old, new):
"""
:param torrent_id: the torrent_id
@ -144,10 +149,12 @@ class TorrentFolderRenamedEvent(DelugeEvent):
"""
self._args = [torrent_id, old, new]
class TorrentFileRenamedEvent(DelugeEvent):
"""
Emitted when a file within a torrent has been renamed.
"""
def __init__(self, torrent_id, index, name):
"""
:param torrent_id: the torrent_id
@ -159,10 +166,12 @@ class TorrentFileRenamedEvent(DelugeEvent):
"""
self._args = [torrent_id, index, name]
class TorrentFinishedEvent(DelugeEvent):
"""
Emitted when a torrent finishes downloading.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -170,10 +179,12 @@ class TorrentFinishedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentResumedEvent(DelugeEvent):
"""
Emitted when a torrent resumes from a paused state.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -181,13 +192,12 @@ class TorrentResumedEvent(DelugeEvent):
"""
self._args = [torrent_id]
class TorrentFileCompletedEvent(DelugeEvent):
"""
Emitted when a file completes.
This will only work with libtorrent 0.15 or greater.
"""
def __init__(self, torrent_id, index):
"""
:param torrent_id: the torrent_id
@ -197,17 +207,36 @@ 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.
"""
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.
"""
def __init__(self, new_release):
"""
:param new_release: the new version that is available
@ -215,29 +244,37 @@ class NewVersionAvailableEvent(DelugeEvent):
"""
self._args = [new_release]
class SessionStartedEvent(DelugeEvent):
"""
Emitted when a session has started. This typically only happens once when
the daemon is initially started.
"""
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.
"""
def __init__(self, key, value):
"""
:param key: the key that changed
@ -246,17 +283,42 @@ class ConfigValueChangedEvent(DelugeEvent):
"""
self._args = [key, value]
class PluginEnabledEvent(DelugeEvent):
"""
Emitted when a plugin is enabled in the Core.
"""
def __init__(self, plugin_name):
self._args = [plugin_name]
class PluginDisabledEvent(DelugeEvent):
"""
Emitted when a plugin is disabled in the Core.
"""
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,207 +1,330 @@
#
# 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 cgi
import logging
import os.path
import zlib
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.python.failure import Failure
from twisted.web import client, http
from twisted.web._newclient import HTTPClientParser
from twisted.web.error import PageRedirect
from twisted.web.http_headers import Headers
from twisted.web.iweb import IAgent
from zope.interface import implementer
from deluge.common import get_version
log = logging.getLogger(__name__)
class HTTPDownloader(client.HTTPDownloader):
"""
Factory class for downloading files and keeping track of progress.
"""
def __init__(self, url, filename, part_callback=None, headers=None,
force_filename=False, allow_compression=True):
class CompressionDecoder(client.GzipDecoder):
"""A compression decoder for gzip, x-gzip and deflate."""
def deliverBody(self, protocol): # NOQA: N802
self.original.deliverBody(CompressionDecoderProtocol(protocol, self.original))
class CompressionDecoderProtocol(client._GzipProtocol):
"""A compression decoder protocol for CompressionDecoder."""
def __init__(self, protocol, response):
super(CompressionDecoderProtocol, self).__init__(protocol, response)
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
class BodyHandler(HTTPClientParser, object):
"""An HTTP parser that saves the response to a file."""
def __init__(self, request, finished, length, agent, encoding=None):
"""BodyHandler init.
Args:
request (t.w.i.IClientRequest): The parser request.
finished (Deferred): A Deferred to handle the finished response.
length (int): The length of the response.
agent (t.w.i.IAgent): The agent from which the request was sent.
"""
:param url: the url to download from
:type url: string
:param filename: the filename to save the file as
:type filename: string
:param force_filename: forces use of the supplied filename, regardless of header content
:type force_filename: bool
:param part_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 part_callback: function
:param headers: any optional headers to send
:type headers: dictionary
"""
self.part_callback = part_callback
super(BodyHandler, self).__init__(request, finished)
self.agent = agent
self.finished = finished
self.total_length = length
self.current_length = 0
self.decoder = None
self.value = filename
self.data = b''
self.encoding = encoding
def dataReceived(self, data): # NOQA: N802
self.current_length += len(data)
self.data += data
if self.agent.part_callback:
self.agent.part_callback(data, self.current_length, self.total_length)
def connectionLost(self, reason): # NOQA: N802
if self.encoding:
self.data = self.data.decode(self.encoding).encode('utf8')
with open(self.agent.filename, 'wb') as _file:
_file.write(self.data)
self.finished.callback(self.agent.filename)
self.state = u'DONE'
HTTPClientParser.connectionLost(self, reason)
@implementer(IAgent)
class HTTPDownloaderAgent(object):
"""A File Downloader Agent."""
def __init__(
self,
agent,
filename,
part_callback=None,
force_filename=False,
allow_compression=True,
handle_redirect=True,
):
"""HTTPDownloaderAgent init.
Args:
agent (t.w.c.Agent): The agent which will send the requests.
filename (str): The filename to save the file as.
force_filename (bool): Forces use of the supplied filename,
regardless of header content.
part_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)
"""
self.handle_redirect = handle_redirect
self.agent = agent
self.filename = filename
self.part_callback = part_callback
self.force_filename = force_filename
self.allow_compression = allow_compression
agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
self.decoder = None
def gotStatus(self, version, status, message):
self.code = int(status)
client.HTTPDownloader.gotStatus(self, version, status, message)
def request_callback(self, response):
finished = Deferred()
def gotHeaders(self, headers):
if self.code == http.OK:
if "content-length" in headers:
self.total_length = int(headers["content-length"][0])
else:
self.total_length = 0
if not self.handle_redirect and response.code in (
http.MOVED_PERMANENTLY,
http.FOUND,
http.SEE_OTHER,
http.TEMPORARY_REDIRECT,
):
location = response.headers.getRawHeaders(b'location')[0]
error = PageRedirect(response.code, location=location)
finished.errback(Failure(error))
else:
headers = response.headers
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
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 headers.hasHeader(b'content-disposition') and not self.force_filename:
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
'utf-8'
)
content_disp_params = cgi.parse_header(content_disp)[1]
if 'filename' in content_disp_params:
new_file_name = content_disp_params['filename']
new_file_name = sanitise_filename(new_file_name)
new_file_name = os.path.join(
os.path.split(self.filename)[0], new_file_name
)
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)
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)
count += 1
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)
count += 1
self.filename = new_file_name
self.fileName = new_file_name
self.value = new_file_name
cont_type = headers.getRawHeaders(b'content-type')[0].decode()
params = cgi.parse_header(cont_type)[1]
encoding = params.get('charset', None)
response.deliverBody(
BodyHandler(response.request, finished, body_length, self, encoding)
)
elif self.code in (http.MOVED_PERMANENTLY, http.FOUND, http.SEE_OTHER, http.TEMPORARY_REDIRECT):
location = headers["location"][0]
error = PageRedirect(self.code, location=location)
self.noPage(Failure(error))
return finished
return client.HTTPDownloader.gotHeaders(self, headers)
def request(self, method, uri, headers=None, body_producer=None):
"""Issue a new request to the wrapped agent.
def pagePart(self, data):
if self.code == http.OK:
self.current_length += len(data)
if self.decoder:
data = self.decoder.decompress(data)
if self.part_callback:
self.part_callback(data, self.current_length, self.total_length)
Args:
method (bytes): The HTTP method to use.
uri (bytes): The url to download from.
headers (t.w.h.Headers, optional): Any extra headers to send.
body_producer (t.w.i.IBodyProducer, optional): Request body data.
return client.HTTPDownloader.pagePart(self, data)
Returns:
Deferred: The filename of the of the downloaded file.
"""
if headers is None:
headers = Headers()
def pageEnd(self):
if self.decoder:
data = self.decoder.flush()
self.current_length -= len(data)
self.decoder = None
self.pagePart(data)
if not headers.hasHeader(b'User-Agent'):
version = get_version()
user_agent = 'Deluge/%s (https://deluge-torrent.org)' % version
headers.addRawHeader('User-Agent', user_agent)
d = self.agent.request(
method=method, uri=uri, headers=headers, bodyProducer=body_producer
)
d.addCallback(self.request_callback)
return d
return client.HTTPDownloader.pageEnd(self)
def sanitise_filename(filename):
"""
Sanitises a filename to use as a download destination file.
"""Sanitises a filename to use as a download destination file.
Logs any filenames that could be considered malicious.
:param filename: the filename to sanitise
:type filename: string
:returns: the sanitised filename
:rtype: string
filename (str): The filename to sanitise.
Returns:
str: The sanitised 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,
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 partial 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 using the filename specified rather than
one the server may suggest.
allow_compression (bool): Allows gzip & deflate decoding.
Returns:
Deferred: The filename of the downloaded file.
Raises:
t.w.e.PageRedirect
t.w.e.Error: for all other HTTP response errors
"""
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.
: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
:returns: the filename of the downloaded file
:rtype: Deferred
: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)
agent = client.Agent(reactor)
if allow_compression:
if not headers:
headers = {}
headers["accept-encoding"] = "deflate, gzip, x-gzip"
enc_accepted = ['gzip', 'x-gzip', 'deflate']
decoders = [(enc.encode(), CompressionDecoder) for enc in enc_accepted]
agent = client.ContentDecoderAgent(agent, decoders)
if handle_redirects:
agent = client.RedirectAgent(agent)
scheme, host, port, path = client._parse(url)
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
if scheme == "https":
from twisted.internet import ssl
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
else:
reactor.connectTCP(host, port, factory)
agent = HTTPDownloaderAgent(
agent, filename, callback, force_filename, allow_compression, handle_redirects
)
return factory.deferred
# The Headers init expects dict values to be a list.
if headers:
for name, value in list(headers.items()):
if not isinstance(value, list):
headers[name] = [value]
return agent.request(b'GET', url.encode(), Headers(headers))
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 partial 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 the filename specified rather than one the
server may suggest.
allow_compression (bool): Allows gzip & deflate decoding.
handle_redirects (bool): HTTP redirects handled automatically or not.
Returns:
Deferred: The filename of the downloaded file.
Raises:
t.w.e.PageRedirect: If handle_redirects is False.
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):
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,
handle_redirects=handle_redirects,
)
d.addCallbacks(on_download_success, on_download_fail)
return d

15
deluge/i18n/__init__.py Normal file
View File

@ -0,0 +1,15 @@
from .util import (
I18N_DOMAIN,
get_languages,
set_language,
setup_mock_translation,
setup_translation,
)
__all__ = [
'I18N_DOMAIN',
'set_language',
'get_languages',
'setup_translation',
'setup_mock_translation',
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

117
deluge/i18n/languages.py Normal file
View File

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
#
# This file is public domain.
#
from __future__ import unicode_literals
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
# Deferred translation
def _(message):
return message
# Languages we provide translations for, out of the box.
LANGUAGES = {
'af': _('Afrikaans'),
'ar': _('Arabic'),
'ast': _('Asturian'),
'az': _('Azerbaijani'),
'bg': _('Bulgarian'),
'be': _('Belarusian'),
'bn': _('Bengali'),
'br': _('Breton'),
'bs': _('Bosnian'),
'ca': _('Catalan'),
'cs': _('Czech'),
'cy': _('Welsh'),
'da': _('Danish'),
'de': _('German'),
'el': _('Greek'),
'en': _('English'),
'en_AU': _('English (Australia)'),
'en_CA': _('English (Canada)'),
'en_GB': _('English (United Kingdom)'),
'eo': _('Esperanto'),
'es': _('Spanish'),
'es-ar': _('Argentinian Spanish'),
'es-mx': _('Mexican Spanish'),
'es-ni': _('Nicaraguan Spanish'),
'es-ve': _('Venezuelan Spanish'),
'et': _('Estonian'),
'eu': _('Basque'),
'fa': _('Persian'),
'fi': _('Finnish'),
'fr': _('French'),
'fy': _('Frisian'),
'ga': _('Irish'),
'gl': _('Galician'),
'he': _('Hebrew'),
'hi': _('Hindi'),
'hr': _('Croatian'),
'hu': _('Hungarian'),
'ia': _('Interlingua'),
'id': _('Indonesian'),
'is': _('Icelandic'),
'it': _('Italian'),
'iu': _('Inuktitut'),
'ja': _('Japanese'),
'ka': _('Georgian'),
'kk': _('Kazakh'),
'km': _('Khmer'),
'kn': _('Kannada'),
'ko': _('Korean'),
'ku': _('Kurdish'),
'la': _('Latin'),
'lb': _('Luxembourgish'),
'lt': _('Lithuanian'),
'lv': _('Latvian'),
'mk': _('Macedonian'),
'ml': _('Malayalam'),
'mn': _('Mongolian'),
'ms': _('Mayaly'),
'my': _('Burmese'),
'nb': _('Norwegian Bokmal'),
'ne': _('Nepali'),
'nds': _('Low German'),
'nl': _('Dutch'),
'nn': _('Norwegian Nynorsk'),
'os': _('Ossetic'),
'pa': _('Punjabi'),
'pl': _('Polish'),
'pms': _('Piedmontese'),
'pt': _('Portuguese'),
'pt_BR': _('Brazilian Portuguese'),
'ro': _('Romanian'),
'ru': _('Russian'),
'sk': _('Slovak'),
'sl': _('Slovenian'),
'si': _('Sinhalese'),
'sq': _('Albanian'),
'sr': _('Serbian'),
'sr-latn': _('Serbian Latin'),
'sv': _('Swedish'),
'sw': _('Swahili'),
'ta': _('Tamil'),
'te': _('Telugu'),
'th': _('Thai'),
'tl': _('Tagalog'),
'tlh': _('Klingon'),
'tr': _('Turkish'),
'tt': _('Tatar'),
'udm': _('Udmurt'),
'uk': _('Ukrainian'),
'ur': _('Urdu'),
'vi': _('Vietnamese'),
'zh_CN': _('Chinese (Simplified)'),
'zh_HK': _('Chinese (Hong Kong)'),
'zh-hans': _('Simplified Chinese'),
'zh-hant': _('Traditional Chinese'),
'zh_TW': _('Chinese (Taiwan)'),
}
del _

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

150
deluge/i18n/util.py Normal file
View File

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 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 __future__ import unicode_literals
import ctypes
import gettext
import locale
import logging
import os
import sys
from glob import glob
from six.moves import builtins
import deluge.common
from .languages import LANGUAGES
log = logging.getLogger(__name__)
I18N_DOMAIN = 'deluge'
def get_translations_path():
"""Get the absolute path to the directory containing translation files"""
return deluge.common.resource_filename('deluge', 'i18n')
def get_languages():
lang = []
translations_path = get_translations_path()
lc_messages_path = os.path.join('LC_MESSAGES', I18N_DOMAIN + '.mo')
for root, dirs, files in os.walk(translations_path):
# Get the dirs
lang_dirs = [
lang_dir
for lang_dir in dirs
if glob(os.path.join(translations_path, lang_dir, lc_messages_path))
]
break
else:
return lang
for i, lang_code in enumerate(lang_dirs):
name = '%s (Language name missing)' % lang_code
if lang_code in LANGUAGES:
name = LANGUAGES[lang_code]
lang.append([lang_code, _(name)])
lang = sorted(lang, key=lambda l: l[1])
return lang
def set_language(lang):
"""
Set the language to use.
gettext and GtkBuilder will load the translations from the specified
language.
:param lang: the language, e.g. "en", "de" or "en_GB"
:type lang: str
"""
# Necessary to set these environment variables for GtkBuilder
deluge.common.set_env_variable('LANGUAGE', lang) # Windows/Linux
deluge.common.set_env_variable('LANG', lang) # For OSX
translations_path = get_translations_path()
try:
ro = gettext.translation(
'deluge', localedir=translations_path, languages=[lang]
)
ro.install()
except IOError as ex:
log.warning('IOError when loading translations: %s', ex)
def setup_mock_translation(warn_msg=None):
def _func(*txt):
if warn_msg:
log.warning(
'"%s" has been marked for translation, but translation is unavailable.',
txt[0],
)
return txt[0]
builtins.__dict__['_'] = _func
builtins.__dict__['ngettext'] = builtins.__dict__['_n'] = _func
# Initialize gettext
def setup_translation():
translations_path = get_translations_path()
log.info('Setting up translations from %s', translations_path)
try:
if hasattr(locale, 'bindtextdomain'):
locale.bindtextdomain(I18N_DOMAIN, translations_path)
if hasattr(locale, 'textdomain'):
locale.textdomain(I18N_DOMAIN)
gettext.bindtextdomain(I18N_DOMAIN, translations_path)
gettext.bind_textdomain_codeset(I18N_DOMAIN, 'UTF-8')
gettext.textdomain(I18N_DOMAIN)
# Workaround for Python 2 unicode gettext (keyword removed in Py3).
kwargs = {} if not deluge.common.PY2 else {'unicode': True}
gettext.install(I18N_DOMAIN, translations_path, names='ngettext', **kwargs)
builtins.__dict__['_n'] = builtins.__dict__['ngettext']
libintl = None
if deluge.common.windows_check():
for intl in ('libintl-8.dll', 'intl.dll'):
try:
libintl = ctypes.cdll.LoadLibrary(intl)
except OSError as ex:
exception = ex
else:
break
finally:
if not libintl:
log.error('Unable to initialize gettext/locale!')
log.error(exception)
setup_mock_translation()
elif deluge.common.osx_check():
libintl = ctypes.cdll.LoadLibrary('libintl.dylib')
if libintl:
libintl.bindtextdomain(
I18N_DOMAIN, translations_path.encode(sys.getfilesystemencoding())
)
libintl.textdomain(I18N_DOMAIN)
libintl.bind_textdomain_codeset(I18N_DOMAIN, 'UTF-8')
libintl.gettext.restype = ctypes.c_char_p
except Exception as ex:
log.error('Unable to initialize gettext/locale!')
log.exception(ex)
setup_mock_translation()
deluge.common.translate_size_units()

View File

@ -1,61 +1,43 @@
#
# 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.
@ -63,10 +45,12 @@ class Logging(LoggingLoggerClass):
if len(logger_name) > MAX_LOGGER_NAME_LENGTH:
MAX_LOGGER_NAME_LENGTH = len(logger_name)
for handler in logging.getLogger().handlers:
handler.setFormatter(logging.Formatter(
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
datefmt="%H:%M:%S"
))
handler.setFormatter(
logging.Formatter(
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH,
datefmt='%H:%M:%S',
)
)
@defer.inlineCallbacks
def garbage(self, msg, *args, **kwargs):
@ -102,84 +86,127 @@ 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'),
defer.__file__.replace('.pyc', '.py')):
if filename in (
__file__.replace('.pyc', '.py'),
defer.__file__.replace('.pyc', '.py'),
):
f = f.f_back
continue
rv = (filename, f.f_lineno, co.co_name)
if common.PY2:
rv = (filename, f.f_lineno, co.co_name)
else:
rv = (filename, f.f_lineno, co.co_name, None)
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')
logging.addLevelName(1, 'GARBAGE')
logging.addLevelName(levels['trace'], 'TRACE')
logging.addLevelName(levels['garbage'], 'GARBAGE')
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"
DEFAULT_LOGGING_FORMAT % MAX_LOGGER_NAME_LENGTH, 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,62 +227,69 @@ 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",
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
log.warning(
'logging.conf found! tweaking logging levels from %s', logging_config_file
)
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.warning('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.
:param level: str, a string representing the desired level
:param logger_name: str, a string representing desired logger name for which
the level should change. The default is "None" will will
tweak the root logger level.
the level should change. The default is "None" will 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
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__', '')
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
module_stack[1], module_stack[2],
caller_module_name)
warnings.warn_explicit(
DEPRECATION_WARNING,
DeprecationWarning,
module_stack[1],
module_stack[2],
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.
@ -268,28 +302,37 @@ If you're the developer, please stop using the above code and instead use:
The above will result in, regarding the "Label" plugin for example a log message similar to:
15:33:54 [deluge.plugins.label.core:78 ][INFO ] *** Start Label plugin ***
Triggering code:"""
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__', '')
warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning,
module_stack[1], module_stack[2],
caller_module_name)
warnings.warn_explicit(
DEPRECATION_WARNING,
DeprecationWarning,
module_stack[1],
module_stack[2],
caller_module_name,
)
if caller_module:
for member in stack:
module = inspect.getmodule(member[0])
if not module:
continue
if module.__name__ in ('deluge.plugins.pluginbase',
'deluge.plugins.init'):
if module.__name__ in (
'deluge.plugins.pluginbase',
'deluge.plugins.init',
):
logger_name += '.plugin.%s' % caller_module_name
# Monkey Patch The Plugin Module
caller_module.log = logging.getLogger(logger_name)
@ -297,8 +340,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,155 +1,133 @@
#
# 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
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.
Note:
Piece sizes must be multiples of 16KiB.
"""
Raised when an invalid piece size is set. 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": {}
}
torrent = {'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
for (dirpath, dirnames, filenames) in os.walk(self.data_path):
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("/")
size = get_path_size(
os.path.join(self.data_path, dirpath, filename)
)
p = dirpath[len(self.data_path) :]
p = p.lstrip('/')
p = p.split('/')
if p[0]:
p += [filename]
else:
@ -160,7 +138,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 +149,224 @@ class TorrentMetadata(object):
fs = []
pieces = []
# Create the piece hashes
buf = ""
buf = b''
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.encode('UTF-8') for s in path]
fs.append({b'length': size, b'path': path})
if path[-1].startswith(b'_____padding_file_'):
buf += b'\0' * size
pieces.append(sha(buf).digest())
buf = ""
fs[-1]["attr"] = "p"
buf = b''
fs[-1][b'attr'] = b'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.encode('utf8'), *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 = b''
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,45 +28,48 @@ 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):
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,
):
data = {'creation date': int(gmtime())}
if url:
data['announce'] = url.strip()
@ -83,31 +83,32 @@ 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)
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,15 +118,15 @@ 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'
with open(f, 'wb') as file_:
file_.write(bencode(utf8_encode_structure(data)))
h.write(bencode(data))
h.close()
def calcsize(path):
total = 0
@ -133,31 +134,13 @@ def calcsize(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,78 +151,86 @@ 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!
fs.append(
{'length': size, 'path': p2, 'content_type': content_type}
) # HEREDAVE. bad for batch!
else:
fs.append({'length': size, 'path': p2})
h = file(f, 'rb')
while pos < size:
a = min(size - pos, piece_length - done)
sh.update(h.read(a))
done += a
pos += a
totalhashed += a
with open(f, 'rb') as file_:
while pos < size:
a = min(size - pos, piece_length - done)
sh.update(file_.read(a))
done += a
pos += a
totalhashed += a
if done == piece_length:
pieces.append(sh.digest())
piece_count += 1
done = 0
sh = sha()
progress(piece_count, num_pieces)
h.close()
if done == piece_length:
pieces.append(sh.digest())
piece_count += 1
done = 0
sh = sha()
progress(piece_count, num_pieces)
if done > 0:
pieces.append(sh.digest())
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')
while p < size:
x = h.read(min(piece_length, size - p))
pieces.append(sha(x).digest())
piece_count += 1
p += piece_length
if p > size:
p = size
progress(piece_count, num_pieces)
h.close()
with open(path, 'rb') as _file:
while p < size:
x = _file.read(min(piece_length, size - p))
pieces.append(sha(x).digest())
piece_count += 1
p += piece_length
if p > size:
p = size
progress(piece_count, num_pieces)
name = os.path.split(path)[1].encode('utf8')
if content_type is not None:
return {'pieces': ''.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}
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': name,
'private': private,
}
def subfiles(d):
r = []

View File

@ -0,0 +1,92 @@
#!/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
from deluge.common import PY2
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:
if PY2:
return os.walk(dirname).__next__[1]
else:
return next(os.walk(dirname))[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,20 @@ 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 +72,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,102 +90,193 @@ 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")
pkg_resources.working_set.add_entry(base_plugin_dir)
user_plugin_dir = os.path.join(deluge.configmanager.get_config_dir(), "plugins")
base_dir = deluge.common.resource_filename('deluge', 'plugins')
user_dir = os.path.join(deluge.configmanager.get_config_dir(), 'plugins')
base_subdir = [
os.path.join(base_dir, f)
for f in os.listdir(base_dir)
if os.path.isdir(os.path.join(base_dir, f))
]
plugin_dirs = [base_dir, user_dir] + base_subdir
plugins_dirs = [base_plugin_dir]
for dirname in os.listdir(base_plugin_dir):
plugin_dir = os.path.join(base_plugin_dir, dirname)
pkg_resources.working_set.add_entry(plugin_dir)
plugins_dirs.append(plugin_dir)
pkg_resources.working_set.add_entry(user_plugin_dir)
plugins_dirs.append(user_plugin_dir)
self.pkg_env = pkg_resources.Environment(plugins_dirs)
for dirname in plugin_dirs:
pkg_resources.working_set.add_entry(dirname)
self.pkg_env = pkg_resources.Environment(plugin_dirs, None)
self.available_plugins = []
for name in self.pkg_env:
log.debug("Found plugin: %s %s at %s",
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.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]
# Activate is required by non-namespace plugins.
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)
cls = egg.load_entry_point(self.entry_name, name)
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
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.warning('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 +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,58 +0,0 @@
#
# __init__.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
from deluge.plugins.init import PluginInitBase
class CorePlugin(PluginInitBase):
def __init__(self, plugin_name):
from core import Core as _plugin_cls
self._plugin_cls = _plugin_cls
super(CorePlugin, self).__init__(plugin_name)
class GtkUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from gtkui import GtkUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(GtkUIPlugin, self).__init__(plugin_name)
class WebUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from webui import WebUI as _plugin_cls
self._plugin_cls = _plugin_cls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -1,43 +0,0 @@
#
# common.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
def get_resource(filename):
import pkg_resources, os
return pkg_resources.resource_filename("deluge.plugins.autoadd",
os.path.join("data", filename))

View File

@ -1,505 +0,0 @@
#
# core.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
from deluge._libtorrent import lt
import os
import logging
from deluge.plugins.pluginbase import CorePluginBase
import deluge.component as component
import deluge.configmanager
from deluge.common import AUTH_LEVEL_ADMIN
from deluge.core.rpcserver import export
from twisted.internet.task import LoopingCall, deferLater
from twisted.internet import reactor
from deluge.event import DelugeEvent
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
"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"
}
MAX_NUM_ATTEMPTS = 10
class AutoaddOptionsChangedEvent(DelugeEvent):
"""Emitted when the options for the plugin are changed."""
def __init__(self):
pass
def CheckInput(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)
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
self.config.save()
self.watchdirs = self.config["watchdirs"]
component.get("EventManager").register_event_handler(
"PreTorrentRemovedEvent", self.__on_pre_torrent_removed
)
# Dict of Filename:Attempts
self.invalid_torrents = {}
# Loopingcall timers for each enabled watchdir
self.update_timers = {}
deferLater(reactor, 5, self.enable_looping)
def enable_looping(self):
# Enable all looping calls for enabled watchdirs here
for watchdir_id, watchdir in self.watchdirs.iteritems():
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
)
for loopingcall in self.update_timers.itervalues():
loopingcall.stop()
self.config.save()
def update(self):
pass
@export
def set_options(self, watchdir_id, options):
"""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.")
)
if options.has_key('path'):
options['abspath'] = os.path.abspath(options['path'])
CheckInput(
os.path.isdir(options['abspath']), _("Path does not exist.")
)
for w_id, w in self.watchdirs.iteritems():
if options['abspath'] == w['abspath'] and watchdir_id != w_id:
raise Exception("Path is already being watched.")
for key in options.keys():
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 watchdir_id in self.update_timers:
self.disable_watchdir(watchdir_id)
self.watchdirs[watchdir_id].update(options)
#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())
def load_torrent(self, filename, magnet):
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
# Get the info to see if any exceptions are raised
if magnet == False:
lt.torrent_info(lt.bdecode(filedump))
return filedump
def split_magnets(self, filename):
log.debug("Attempting to open %s for splitting magnets.", filename)
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"])
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:
_mfile.write(magnet)
_mfile.close()
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)
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.",
watchdir_id)
self.disable_watchdir(watchdir_id)
return
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 = {}
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():
if OPTIONS_AVAILABLE.get(option):
if watchdir.get(option+'_toggle', True):
opts[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"]):
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)
continue
if os.path.isdir(filepath):
# Skip directories
continue
elif os.path.splitext(filename)[1] == ".magnet" and \
self.split_magnets(filepath):
os.remove(filepath)
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)
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:
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)
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")
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")
)
# If the torrent added successfully, set the extra options.
if torrent_id:
if 'Label' in component.get("CorePluginManager").get_enabled_plugins():
if watchdir.get('label_toggle', True) and watchdir.get('label'):
label = component.get("CorePlugin.Label")
if not watchdir['label'] in label.get_labels():
label.add(watchdir['label'])
label.set_torrent(torrent_id, watchdir['label'])
if watchdir.get('queue_to_top_toggle', True) and 'queue_to_top' in watchdir:
if watchdir['queue_to_top']:
component.get("TorrentManager").queue_top(torrent_id)
else:
component.get("TorrentManager").queue_bottom(torrent_id)
else:
# torrent handle is invalid and so is the magnet link
if magnet == True:
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"
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\"",
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
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)
@export
def enable_watchdir(self, watchdir_id):
w_id = str(watchdir_id)
# Enable the looping call
if w_id not in self.update_timers or not self.update_timers[w_id].running:
self.update_timers[w_id] = LoopingCall(self.update_watchdir, w_id)
self.update_timers[w_id].start(5).addErrback(
self.on_update_watchdir_error, w_id
)
# Update the config
if not self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = True
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
@export
def disable_watchdir(self, watchdir_id):
w_id = str(watchdir_id)
# Disable the looping call
if w_id in self.update_timers:
if self.update_timers[w_id].running:
self.update_timers[w_id].stop()
del self.update_timers[w_id]
# Update the config
if self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = False
self.config.save()
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():
self.config[key] = config[key]
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
@export
def get_config(self):
"""Returns the config dictionary."""
return self.config.config
@export
def get_watchdirs(self):
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)
return self.watchdirs
watchdirs = {}
for watchdir_id, watchdir in self.watchdirs.iteritems():
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())
return watchdirs
def _make_unicode(self, options):
opts = {}
for key in options:
if isinstance(options[key], str):
options[key] = unicode(options[key], "utf8")
opts[key] = options[key]
return opts
@export
def add(self, options={}):
"""Add a watch folder."""
options = self._make_unicode(options)
abswatchdir = os.path.abspath(options['path'])
CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist."))
CheckInput(
os.access(abswatchdir, os.R_OK|os.W_OK),
"You must have read and write access to watch folder."
)
if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]:
raise Exception("Path is already being watched.")
options.setdefault('enabled', False)
options['abspath'] = abswatchdir
watchdir_id = self.config['next_id']
self.watchdirs[str(watchdir_id)] = options
if options.get('enabled'):
self.enable_watchdir(watchdir_id)
self.config['next_id'] = watchdir_id + 1
self.config.save()
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)
if self.watchdirs[watchdir_id]['enabled']:
self.disable_watchdir(watchdir_id)
del self.watchdirs[watchdir_id]
self.config.save()
component.get("EventManager").emit(AutoaddOptionsChangedEvent())
def __migrate_config_1_to_2(self, config):
for watchdir_id in config['watchdirs'].iterkeys():
config['watchdirs'][watchdir_id]['owner'] = 'localclient'
return config
def __on_pre_torrent_removed(self, torrent_id):
try:
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",
torrent_id)
return
torrent_fname = torrent.filename
for watchdir in self.watchdirs.itervalues():
if not watchdir.get('copy_torrent_toggle', False):
# This watchlist does copy torrents
continue
elif not watchdir.get('delete_copy_torrent_toggle', False):
# This watchlist is not set to delete finished torrents
continue
copy_torrent_path = watchdir['copy_torrent']
torrent_fname_path = os.path.join(copy_torrent_path, torrent_fname)
if os.path.isfile(torrent_fname_path):
try:
os.remove(torrent_fname_path)
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)

View File

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

View File

@ -1,516 +0,0 @@
#
# gtkui.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
import gtk
import gtk.glade
import logging
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
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"]
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"))
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
})
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
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
self.glade.get_widget('opts_add_button').show()
self.glade.get_widget('opts_apply_button').hide()
self.watchdir_id = None
self.load_options(options)
self.dialog.run()
def load_options(self, options):
self.glade.get_widget('enabled').set_active(options.get('enabled', True))
self.glade.get_widget('append_extension_toggle').set_active(
options.get('append_extension_toggle', False)
)
self.glade.get_widget('append_extension').set_text(
options.get('append_extension', '.added')
)
self.glade.get_widget('download_location_toggle').set_active(
options.get('download_location_toggle', False)
)
self.glade.get_widget('copy_torrent_toggle').set_active(
options.get('copy_torrent_toggle', False)
)
self.glade.get_widget('delete_copy_torrent_toggle').set_active(
options.get('delete_copy_torrent_toggle', False)
)
self.accounts.clear()
self.labels.clear()
combobox = self.glade.get_widget('OwnerCombobox')
combobox_render = gtk.CellRendererText()
combobox.pack_start(combobox_render, True)
combobox.add_attribute(combobox_render, 'text', 0)
combobox.set_model(self.accounts)
label_widget = self.glade.get_widget('label')
label_widget.child.set_text(options.get('label', ''))
label_widget.set_model(self.labels)
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))
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):
self.glade.get_widget('isnt_queue_to_top').set_active(True)
if not options.get('auto_managed', True):
self.glade.get_widget('isnt_auto_managed').set_active(True)
for field in ['move_completed_path', 'path', 'download_location',
'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").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").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"])
)
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"])
)
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"])
)
else:
self.glade.get_widget('download_location_entry').set_text(
options.get('download_location', config["download_location"])
)
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"])
)
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"])
)
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")
selected_iter = None
for account in accounts:
iter = self.accounts.append()
self.accounts.set_value(
iter, 0, account['username']
)
if account['username'] == owner:
selected_iter = 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())
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)
for label in labels:
self.labels.set_value(self.labels.append(), 0, label)
label_widget = self.glade.get_widget('label')
label_widget.set_model(self.labels)
label_widget.set_text_column(0)
def on_failure(failure):
log.exception(failure)
def on_get_enabled_plugins(result):
if 'Label' in result:
self.glade.get_widget('label_frame').show()
client.label.get_labels().addCallback(on_labels).addErrback(on_failure)
else:
self.glade.get_widget('label_frame').hide()
self.glade.get_widget('label_toggle').set_active(False)
client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN:
client.core.get_known_accounts().addCallback(
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())
self.glade.get_widget('OwnerCombobox').set_active(0)
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
def set_sensitive(self):
maintoggles = ['download_location', 'append_extension',
'move_completed', 'label', 'max_download_speed',
'max_upload_speed', 'max_connections',
'max_upload_slots', 'add_paused', 'auto_managed',
'stop_at_ratio', 'queue_to_top', 'copy_torrent']
[self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles]
def on_toggle_toggled(self, tb):
toggle = str(tb.name).replace("_toggle", "")
isactive = tb.get_active()
if toggle == 'download_location':
self.glade.get_widget('download_location_chooser').set_sensitive(isactive)
self.glade.get_widget('download_location_entry').set_sensitive(isactive)
elif toggle == 'append_extension':
self.glade.get_widget('append_extension').set_sensitive(isactive)
elif toggle == 'copy_torrent':
self.glade.get_widget('copy_torrent_entry').set_sensitive(isactive)
self.glade.get_widget('copy_torrent_chooser').set_sensitive(isactive)
self.glade.get_widget('delete_copy_torrent_toggle').set_sensitive(isactive)
elif toggle == 'move_completed':
self.glade.get_widget('move_completed_path_chooser').set_sensitive(isactive)
self.glade.get_widget('move_completed_path_entry').set_sensitive(isactive)
self.glade.get_widget('move_completed').set_active(isactive)
elif toggle == 'label':
self.glade.get_widget('label').set_sensitive(isactive)
elif toggle == 'max_download_speed':
self.glade.get_widget('max_download_speed').set_sensitive(isactive)
elif toggle == 'max_upload_speed':
self.glade.get_widget('max_upload_speed').set_sensitive(isactive)
elif toggle == 'max_connections':
self.glade.get_widget('max_connections').set_sensitive(isactive)
elif toggle == 'max_upload_slots':
self.glade.get_widget('max_upload_slots').set_sensitive(isactive)
elif toggle == 'add_paused':
self.glade.get_widget('add_paused').set_sensitive(isactive)
self.glade.get_widget('isnt_add_paused').set_sensitive(isactive)
elif toggle == 'queue_to_top':
self.glade.get_widget('queue_to_top').set_sensitive(isactive)
self.glade.get_widget('isnt_queue_to_top').set_sensitive(isactive)
elif toggle == 'auto_managed':
self.glade.get_widget('auto_managed').set_sensitive(isactive)
self.glade.get_widget('isnt_auto_managed').set_sensitive(isactive)
elif toggle == 'stop_at_ratio':
self.glade.get_widget('remove_at_ratio_toggle').set_active(isactive)
self.glade.get_widget('stop_ratio_toggle').set_active(isactive)
self.glade.get_widget('stop_at_ratio').set_active(isactive)
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):
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()
def on_error_show(self, result):
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):
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()
def on_cancel(self, Event=None):
self.dialog.destroy()
def generate_opts(self):
# generate options dict based on gtk objects
options = {}
options['enabled'] = self.glade.get_widget('enabled').get_active()
if client.is_localhost():
options['path'] = self.glade.get_widget('path_chooser').get_filename()
options['download_location'] = self.glade.get_widget(
'download_location_chooser').get_filename()
options['move_completed_path'] = self.glade.get_widget(
'move_completed_path_chooser').get_filename()
options['copy_torrent'] = self.glade.get_widget(
'copy_torrent_chooser').get_filename()
else:
options['path'] = self.glade.get_widget('path_entry').get_text()
options['download_location'] = self.glade.get_widget(
'download_location_entry').get_text()
options['move_completed_path'] = self.glade.get_widget(
'move_completed_path_entry').get_text()
options['copy_torrent'] = self.glade.get_widget(
'copy_torrent_entry').get_text()
options['label'] = self.glade.get_widget('label').child.get_text().lower()
options['append_extension'] = self.glade.get_widget('append_extension').get_text()
options['owner'] = self.accounts[
self.glade.get_widget('OwnerCombobox').get_active()][0]
for key in ['append_extension_toggle', 'download_location_toggle',
'label_toggle', 'copy_torrent_toggle',
'delete_copy_torrent_toggle']:
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()
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!"))
return options
class GtkUI(GtkPluginBase):
def enable(self):
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
})
self.opts_dialog = OptionsDialog()
component.get("PluginManager").register_hook(
"on_apply_prefs", self.on_apply_prefs
)
component.get("PluginManager").register_hook(
"on_show_prefs", self.on_show_prefs
)
client.register_event_handler(
"AutoaddOptionsChangedEvent", self.on_options_changed_event
)
self.watchdirs = {}
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)
vbox.pack_start(sw, True, True, 0)
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.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")
)
def disable(self):
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
)
def create_model(self):
store = gtk.ListStore(str, bool, str, str)
for watchdir_id, watchdir in self.watchdirs.iteritems():
store.append([
watchdir_id, watchdir['enabled'],
watchdir.get('owner', 'localclient'), watchdir['path']
])
return store
def create_columns(self, treeView):
rendererToggle = gtk.CellRendererToggle()
column = gtk.TreeViewColumn(
_("Active"), rendererToggle, activatable=1, active=1
)
column.set_sort_column_id(1)
treeView.append_column(column)
tt = gtk.Tooltip()
tt.set_text(_('Double-click to toggle'))
treeView.set_tooltip_cell(tt, None, None, rendererToggle)
rendererText = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Owner"), rendererText, text=2)
column.set_sort_column_id(2)
treeView.append_column(column)
tt2 = gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeView.set_has_tooltip(True)
rendererText = gtk.CellRendererText()
column = gtk.TreeViewColumn(_("Path"), rendererText, text=3)
column.set_sort_column_id(3)
treeView.append_column(column)
tt2 = gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeView.set_has_tooltip(True)
def load_watchdir_list(self):
pass
def add_watchdir_entry(self):
pass
def on_add_button_clicked(self, Event=None):
#display options_window
self.opts_dialog.show()
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):
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 self.watchdirs[watchdir_id]['enabled']:
client.autoadd.disable_watchdir(watchdir_id)
else:
client.autoadd.enable_watchdir(watchdir_id)
else:
self.opts_dialog.show(self.watchdirs[watchdir_id], watchdir_id)
def on_listitem_activated(self, treeview):
tree, tree_id = self.treeView.get_selection().get_selected()
if tree_id:
self.glade.get_widget('edit_button').set_sensitive(True)
self.glade.get_widget('remove_button').set_sensitive(True)
else:
self.glade.get_widget('edit_button').set_sensitive(False)
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():
client.autoadd.set_options(watchdir_id, watchdir)
def on_show_prefs(self):
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
def on_options_changed_event(self):
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
def cb_get_config(self, watchdirs):
"""callback for on show_prefs"""
log.trace("Got whatchdirs from core: %s", watchdirs)
self.watchdirs = watchdirs or {}
self.store.clear()
for watchdir_id, watchdir in self.watchdirs.iteritems():
self.store.append([
watchdir_id, watchdir['enabled'],
watchdir.get('owner', 'localclient'), watchdir['path']
])
# Workaround for cached glade signal appearing when re-enabling plugin in same session
if self.glade.get_widget('edit_button'):
# Disable the remove and edit buttons, because nothing in the store is selected
self.glade.get_widget('remove_button').set_sensitive(False)
self.glade.get_widget('edit_button').set_sensitive(False)

View File

@ -1,57 +0,0 @@
#
# webui.py
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
#
# Deluge is free software.
#
# You may redistribute it and/or modify it under the terms of the
# GNU General Public License, as published by the Free Software
# Foundation; either version 3 of the License, or (at your option)
# any later version.
#
# deluge is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with deluge. If not, write to:
# The Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor
# Boston, MA 02110-1301, USA.
#
# In addition, as a special exception, the copyright holders give
# permission to link the code of portions of this program with the OpenSSL
# library.
# You must obey the GNU General Public License in all respects for all of
# the code used other than OpenSSL. If you modify file(s) with this
# exception, you may extend this exception to your version of the file(s),
# but you are not obligated to do so. If you do not wish to do so, delete
# this exception statement from your version. If you delete this exception
# statement from all source files in the program, then also delete it here.
#
import 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("autoadd.js")]
def enable(self):
pass
def disable(self):
pass

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# 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>
#
# 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 _pluginCls
self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name)
class Gtk3UIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
super(Gtk3UIPlugin, self).__init__(plugin_name)
class WebUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name)

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
#
# 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, subdir=False):
folder = os.path.join('data', 'autoadd_options') if subdir else 'data'
return resource_filename(__package__, os.path.join(folder, filename))

View File

@ -0,0 +1,522 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
#
# 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>
#
# 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 shutil
from base64 import b64encode
from twisted.internet import reactor
from twisted.internet.task import LoopingCall, deferLater
import deluge.component as component
import deluge.configmanager
from deluge._libtorrent import lt
from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
from deluge.core.rpcserver import export
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}
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 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)
self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2)
self.config.save()
self.watchdirs = self.config['watchdirs']
self.rpcserver = component.get('RPCServer')
component.get('EventManager').register_event_handler(
'PreTorrentRemovedEvent', self.__on_pre_torrent_removed
)
# Dict of Filename:Attempts
self.invalid_torrents = {}
# Loopingcall timers for each enabled watchdir
self.update_timers = {}
deferLater(reactor, 5, self.enable_looping)
def enable_looping(self):
# Enable all looping calls for enabled watchdirs here
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
)
for loopingcall in self.update_timers.values():
loopingcall.stop()
self.config.save()
def update(self):
pass
@export
def set_options(self, watchdir_id, options):
"""Update the options for a watch folder."""
watchdir_id = str(watchdir_id)
options = self._make_unicode(options)
check_input(watchdir_id in self.watchdirs, _('Watch folder does not exist.'))
if 'path' in options:
options['abspath'] = os.path.abspath(options['path'])
check_input(os.path.isdir(options['abspath']), _('Path does not exist.'))
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:
if key not in OPTIONS_AVAILABLE:
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
if self.watchdirs[watchdir_id]['enabled']:
self.enable_watchdir(watchdir_id)
self.config.save()
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:
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 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)
magnets = []
try:
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:
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)
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)
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.', watchdir_id)
self.disable_watchdir(watchdir_id)
return
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
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.items():
if OPTIONS_AVAILABLE.get(option):
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']):
try:
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
):
os.remove(filepath)
for filename in os.listdir(watchdir['abspath']):
try:
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
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 (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')
del self.invalid_torrents[filename]
else:
self.invalid_torrents[filename] = 1
continue
def on_torrent_added(torrent_id, filename, filepath):
if 'Label' in component.get('CorePluginManager').get_enabled_plugins():
if watchdir.get('label_toggle', True) and watchdir.get('label'):
label = component.get('CorePlugin.Label')
if not watchdir['label'] in label.get_labels():
label.add(watchdir['label'])
try:
label.set_torrent(torrent_id, watchdir['label'])
except Exception as ex:
log.error('Unable to set label: %s', ex)
if (
watchdir.get('queue_to_top_toggle', True)
and 'queue_to_top' in watchdir
):
if watchdir['queue_to_top']:
component.get('TorrentManager').queue_top(torrent_id)
else:
component.get('TorrentManager').queue_bottom(torrent_id)
# Rename, 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'
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"',
os.path.basename(filepath),
copy_torrent_path,
)
shutil.move(filepath, copy_torrent_file)
else:
os.remove(filepath)
def fail_torrent_add(err_msg, filepath, magnet):
# torrent handle is invalid and so is the magnet link
log.error(
'Cannot Autoadd %s: %s: %s',
'magnet' if magnet else 'torrent file',
filepath,
err_msg,
)
os.rename(filepath, filepath + '.invalid')
try:
# The torrent looks good, so lets add it to the session.
if magnet:
d = component.get('Core').add_torrent_magnet(
filedump.strip(), options
)
else:
d = component.get('Core').add_torrent_file_async(
filename, b64encode(filedump), options
)
d.addCallback(on_torrent_added, filename, filepath)
d.addErrback(fail_torrent_add, filepath, magnet)
except AddTorrentError as ex:
fail_torrent_add(str(ex), filepath, magnet)
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,
)
@export
def enable_watchdir(self, watchdir_id):
w_id = str(watchdir_id)
# Enable the looping call
if w_id not in self.update_timers or not self.update_timers[w_id].running:
self.update_timers[w_id] = LoopingCall(self.update_watchdir, w_id)
self.update_timers[w_id].start(5).addErrback(
self.on_update_watchdir_error, w_id
)
# Update the config
if not self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = True
self.config.save()
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def disable_watchdir(self, watchdir_id):
w_id = str(watchdir_id)
# Disable the looping call
if w_id in self.update_timers:
if self.update_timers[w_id].running:
self.update_timers[w_id].stop()
del self.update_timers[w_id]
# Update the config
if self.watchdirs[w_id]['enabled']:
self.watchdirs[w_id]['enabled'] = False
self.config.save()
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def set_config(self, config):
"""Sets the config dictionary."""
config = self._make_unicode(config)
for key in config:
self.config[key] = config[key]
self.config.save()
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
@export
def get_config(self):
"""Returns the config dictionary."""
return self.config.config
@export
def get_watchdirs(self):
session_user = self.rpcserver.get_session_user()
session_auth_level = self.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,
)
return self.watchdirs
watchdirs = {}
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 '
'their watchdirs: %s',
session_user,
list(watchdirs),
)
return watchdirs
def _make_unicode(self, options):
opts = {}
for key in options:
if isinstance(options[key], bytes):
options[key] = options[key].decode('utf8')
opts[key] = options[key]
return opts
@export
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'])
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.values()]:
raise Exception('Path is already being watched.')
options.setdefault('enabled', False)
options['abspath'] = abswatchdir
watchdir_id = self.config['next_id']
self.watchdirs[str(watchdir_id)] = options
if options.get('enabled'):
self.enable_watchdir(watchdir_id)
self.config['next_id'] = watchdir_id + 1
self.config.save()
component.get('EventManager').emit(AutoaddOptionsChangedEvent())
return watchdir_id
@export
def remove(self, watchdir_id):
"""Remove a watch folder."""
watchdir_id = str(watchdir_id)
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())
def __migrate_config_1_to_2(self, config):
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]
except KeyError:
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.values():
if not watchdir.get('copy_torrent_toggle', False):
# This watchlist does copy torrents
continue
elif not watchdir.get('delete_copy_torrent_toggle', False):
# This watchlist is not set to delete finished torrents
continue
copy_torrent_path = watchdir['copy_torrent']
torrent_fname_path = os.path.join(copy_torrent_path, torrent_fname)
if os.path.isfile(torrent_fname_path):
try:
os.remove(torrent_fname_path)
log.info(
'Removed torrent file "%s" from "%s"',
torrent_fname,
copy_torrent_path,
)
break
except OSError as ex:
log.info(
'Failed to removed torrent file "%s" from "%s": %s',
torrent_fname,
copy_torrent_path,
ex,
)
@export
def is_admin_level(self):
return self.rpcserver.get_session_auth_level() == deluge.common.AUTH_LEVEL_ADMIN
@export
def get_auth_user(self):
return self.rpcserver.get_session_user()

View File

@ -0,0 +1,239 @@
/**
* 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.
*/
Ext.ns('Deluge.ux.AutoAdd');
Deluge.ux.AutoAdd.onClickFunctions = {};
Ext.ns('Deluge.ux.preferences');
/**
* @class Deluge.ux.preferences.AutoAddPage
* @extends Ext.Panel
*/
Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
title: _('AutoAdd'),
header: false,
layout: 'fit',
border: false,
watchdirs: {},
initComponent: function() {
Deluge.ux.preferences.AutoAddPage.superclass.initComponent.call(this);
var autoAdd = this;
this.list = new Ext.list.ListView({
store: new Ext.data.JsonStore({
fields: ['id', 'enabled', 'owner', 'path'],
}),
columns: [
{
id: 'enabled',
header: _('Active'),
sortable: true,
dataIndex: 'enabled',
tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', {
getCheckbox: function(checked, selected) {
Deluge.ux.AutoAdd.onClickFunctions[
selected.id
] = function() {
if (selected.enabled) {
deluge.client.autoadd.disable_watchdir(
selected.id
);
checked = false;
} else {
deluge.client.autoadd.enable_watchdir(
selected.id
);
checked = true;
}
autoAdd.updateWatchDirs();
};
return (
'<input id="enabled-' +
selected.id +
'" type="checkbox"' +
(checked ? ' checked' : '') +
' onclick="Deluge.ux.AutoAdd.onClickFunctions[' +
selected.id +
']()" />'
);
},
}),
width: 0.15,
},
{
id: 'owner',
header: _('Owner'),
sortable: true,
dataIndex: 'owner',
width: 0.2,
},
{
id: 'path',
header: _('Path'),
sortable: true,
dataIndex: 'path',
},
],
singleSelect: true,
autoExpandColumn: 'path',
});
this.list.on('selectionchange', this.onSelectionChange, 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,
},
],
},
});
this.on('show', this.onPreferencesShow, this);
},
updateWatchDirs: function() {
deluge.client.autoadd.get_watchdirs({
success: function(watchdirs) {
this.watchdirs = watchdirs;
var watchdirsArray = [];
for (var id in watchdirs) {
if (watchdirs.hasOwnProperty(id)) {
var watchdir = {};
watchdir['id'] = id;
watchdir['enabled'] = watchdirs[id].enabled;
watchdir['owner'] =
watchdirs[id].owner || 'localclient';
watchdir['path'] = watchdirs[id].path;
watchdirsArray.push(watchdir);
}
}
this.list.getStore().loadData(watchdirsArray);
},
scope: this,
});
},
onAddClick: function() {
if (!this.addWin) {
this.addWin = new Deluge.ux.AutoAdd.AddAutoAddCommandWindow();
this.addWin.on(
'watchdiradd',
function() {
this.updateWatchDirs();
},
this
);
}
this.addWin.show();
},
onEditClick: function() {
if (!this.editWin) {
this.editWin = new Deluge.ux.AutoAdd.EditAutoAddCommandWindow();
this.editWin.on(
'watchdiredit',
function() {
this.updateWatchDirs();
},
this
);
}
var id = this.list.getSelectedRecords()[0].id;
this.editWin.show(id, this.watchdirs[id]);
},
onPreferencesShow: function() {
this.updateWatchDirs();
},
onRemoveClick: function() {
var record = this.list.getSelectedRecords()[0];
deluge.client.autoadd.remove(record.id, {
success: function() {
this.updateWatchDirs();
},
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.AutoAddPlugin = Ext.extend(Deluge.Plugin, {
name: 'AutoAdd',
static: {
prefsPage: null,
},
onDisable: function() {
deluge.preferences.removePage(Deluge.plugins.AutoAddPlugin.prefsPage);
Deluge.plugins.AutoAddPlugin.prefsPage = null;
},
onEnable: function() {
/*
* Called for each of the JavaScript files.
* This will prevent adding unnecessary tabs to the preferences window.
*/
if (!Deluge.plugins.AutoAddPlugin.prefsPage) {
Deluge.plugins.AutoAddPlugin.prefsPage = deluge.preferences.addPage(
new Deluge.ux.preferences.AutoAddPage()
);
}
},
});
Deluge.registerPlugin('AutoAdd', Deluge.plugins.AutoAddPlugin);

View File

@ -0,0 +1,475 @@
/**
* 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.
*/
Ext.ns('Deluge.ux.AutoAdd');
/**
* @class Deluge.ux.AutoAdd.AutoAddWindowBase
* @extends Ext.Window
*/
Deluge.ux.AutoAdd.AutoAddWindowBase = Ext.extend(Ext.Window, {
width: 350,
autoHeight: true,
closeAction: 'hide',
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',
],
toggle_ids: [
'append_extension_toggle',
'download_location_toggle',
'label_toggle',
'copy_torrent_toggle',
'delete_copy_torrent_toggle',
'seed_mode',
],
accounts: new Ext.data.ArrayStore({
storeId: 'accountStore',
id: 0,
fields: [
{
name: 'displayText',
type: 'string',
},
],
}),
labels: new Ext.data.ArrayStore({
storeId: 'labelStore',
id: 0,
fields: [
{
name: 'displayText',
type: 'string',
},
],
}),
initComponent: function() {
Deluge.ux.AutoAdd.AutoAddWindowBase.superclass.initComponent.call(this);
this.addButton(_('Cancel'), this.onCancelClick, this);
this.MainTab = new Deluge.ux.AutoAdd.AutoAddMainPanel();
this.OptionsTab = new Deluge.ux.AutoAdd.AutoAddOptionsPanel();
this.form = this.add({
xtype: 'form',
baseCls: 'x-plain',
bodyStyle: 'padding: 5px',
items: [
{
xtype: 'tabpanel',
activeTab: 0,
items: [this.MainTab, this.OptionsTab],
},
],
});
},
onCancelClick: function() {
this.hide();
},
getOptions: function() {
var options = {};
options['enabled'] = Ext.getCmp('enabled').getValue();
options['path'] = Ext.getCmp('path').getValue();
options['download_location'] = Ext.getCmp(
'download_location'
).getValue();
options['move_completed_path'] = Ext.getCmp(
'move_completed_path'
).getValue();
options['copy_torrent'] = Ext.getCmp('copy_torrent').getValue();
options['label'] = Ext.getCmp('label').getValue();
options['append_extension'] = Ext.getCmp('append_extension').getValue();
options['owner'] = Ext.getCmp('owner').getValue();
this.toggle_ids.forEach(function(toggle_id) {
options[toggle_id] = Ext.getCmp(toggle_id).getValue();
});
this.spin_ids.forEach(function(spin_id) {
options[spin_id] = Ext.getCmp(spin_id).getValue();
options[spin_id + '_toggle'] = Ext.getCmp(
spin_id + '_toggle'
).getValue();
});
this.spin_int_ids.forEach(function(spin_int_id) {
options[spin_int_id] = Ext.getCmp(spin_int_id).getValue();
options[spin_int_id + '_toggle'] = Ext.getCmp(
spin_int_id + '_toggle'
).getValue();
});
this.chk_ids.forEach(function(chk_id) {
options[chk_id] = Ext.getCmp(chk_id).getValue();
options[chk_id + '_toggle'] = Ext.getCmp(
chk_id + '_toggle'
).getValue();
});
if (
options['copy_torrent_toggle'] &&
options['path'] === options['copy_torrent']
) {
throw _(
'"Watch Folder" directory and "Copy of .torrent' +
' files to" directory cannot be the same!'
);
}
return options;
},
loadOptions: function(options) {
/*
* Populate all available options data to the UI
*/
var value;
if (options === undefined) {
options = {};
}
Ext.getCmp('enabled').setValue(
options['enabled'] !== undefined ? options['enabled'] : true
);
Ext.getCmp('isnt_append_extension').setValue(true);
Ext.getCmp('append_extension_toggle').setValue(
options['append_extension_toggle'] !== undefined
? options['append_extension_toggle']
: false
);
Ext.getCmp('append_extension').setValue(
options['append_extension'] !== undefined
? options['append_extension']
: '.added'
);
Ext.getCmp('download_location_toggle').setValue(
options['download_location_toggle'] !== undefined
? options['download_location_toggle']
: false
);
Ext.getCmp('copy_torrent_toggle').setValue(
options['copy_torrent_toggle'] !== undefined
? options['copy_torrent_toggle']
: false
);
Ext.getCmp('delete_copy_torrent_toggle').setValue(
options['delete_copy_torrent_toggle'] !== undefined
? options['delete_copy_torrent_toggle']
: false
);
value =
options['seed_mode'] !== undefined ? options['seed_mode'] : false;
Ext.getCmp('seed_mode').setValue(value);
this.accounts.removeAll(true);
this.labels.removeAll(true);
Ext.getCmp('owner').store = this.accounts;
Ext.getCmp('label').store = this.labels;
Ext.getCmp('label').setValue(
options['label'] !== undefined ? options['label'] : ''
);
Ext.getCmp('label_toggle').setValue(
options['label_toggle'] !== undefined
? options['label_toggle']
: false
);
this.spin_ids.forEach(function(spin_id) {
Ext.getCmp(spin_id).setValue(
options[spin_id] !== undefined ? options[spin_id] : 0
);
Ext.getCmp(spin_id + '_toggle').setValue(
options[spin_id + '_toggle'] !== undefined
? options[spin_id + '_toggle']
: false
);
});
this.chk_ids.forEach(function(chk_id) {
Ext.getCmp(chk_id).setValue(
options[chk_id] !== undefined ? options[chk_id] : true
);
Ext.getCmp(chk_id + '_toggle').setValue(
options[chk_id + '_toggle'] !== undefined
? options[chk_id + '_toggle']
: false
);
});
value =
options['add_paused'] !== undefined ? options['add_paused'] : true;
if (!value) {
Ext.getCmp('not_add_paused').setValue(true);
}
value =
options['queue_to_top'] !== undefined
? options['queue_to_top']
: true;
if (!value) {
Ext.getCmp('not_queue_to_top').setValue(true);
}
value =
options['auto_managed'] !== undefined
? options['auto_managed']
: true;
if (!value) {
Ext.getCmp('not_auto_managed').setValue(true);
}
[
'move_completed_path',
'path',
'download_location',
'copy_torrent',
].forEach(function(field) {
value = options[field] !== undefined ? options[field] : '';
Ext.getCmp(field).setValue(value);
});
if (Object.keys(options).length === 0) {
deluge.client.core.get_config({
success: function(config) {
var value;
Ext.getCmp('download_location').setValue(
options['download_location'] !== undefined
? options['download_location']
: config['download_location']
);
value =
options['move_completed_toggle'] !== undefined
? options['move_completed_toggle']
: config['move_completed'];
if (value) {
Ext.getCmp('move_completed_toggle').setValue(
options['move_completed_toggle'] !== undefined
? options['move_completed_toggle']
: false
);
Ext.getCmp('move_completed_path').setValue(
options['move_completed_path'] !== undefined
? options['move_completed_path']
: config['move_completed_path']
);
}
value =
options['copy_torrent_toggle'] !== undefined
? options['copy_torrent_toggle']
: config['copy_torrent_file'];
if (value) {
Ext.getCmp('copy_torrent_toggle').setValue(true);
Ext.getCmp('copy_torrent').setValue(
options['copy_torrent'] !== undefined
? options['copy_torrent']
: config['torrentfiles_location']
);
}
value =
options['delete_copy_torrent_toggle'] !== undefined
? options['copy_torrent_toggle']
: config['del_copy_torrent_file'];
if (value) {
Ext.getCmp('delete_copy_torrent_toggle').setValue(true);
}
},
});
}
deluge.client.core.get_enabled_plugins({
success: function(plugins) {
if (plugins !== undefined && plugins.indexOf('Label') > -1) {
this.MainTab.LabelFset.setVisible(true);
deluge.client.label.get_labels({
success: function(labels) {
for (
var index = 0;
index < labels.length;
index++
) {
labels[index] = [labels[index]];
}
this.labels.loadData(labels, false);
},
failure: function(failure) {
console.error(failure);
},
scope: this,
});
} else {
this.MainTab.LabelFset.setVisible(false);
}
},
scope: this,
});
var me = this;
function on_accounts(accounts, owner) {
for (var index = 0; index < accounts.length; index++) {
accounts[index] = [accounts[index]['username']];
}
me.accounts.loadData(accounts, false);
Ext.getCmp('owner')
.setValue(owner)
.enable();
}
function on_accounts_failure(failure) {
deluge.client.autoadd.get_auth_user({
success: function(user) {
me.accounts.loadData([[user]], false);
Ext.getCmp('owner')
.setValue(user)
.disable(true);
},
scope: this,
});
}
deluge.client.autoadd.is_admin_level({
success: function(is_admin) {
if (is_admin) {
deluge.client.core.get_known_accounts({
success: function(accounts) {
deluge.client.autoadd.get_auth_user({
success: function(user) {
on_accounts(
accounts,
options['owner'] !== undefined
? options['owner']
: user
);
},
scope: this,
});
},
failure: on_accounts_failure,
scope: this,
});
} else {
on_accounts_failure(null);
}
},
scope: this,
});
},
});
/**
* @class Deluge.ux.AutoAdd.EditAutoAddCommandWindow
* @extends Deluge.ux.AutoAdd.AutoAddWindowBase
*/
Deluge.ux.AutoAdd.EditAutoAddCommandWindow = Ext.extend(
Deluge.ux.AutoAdd.AutoAddWindowBase,
{
title: _('Edit Watch Folder'),
initComponent: function() {
Deluge.ux.AutoAdd.EditAutoAddCommandWindow.superclass.initComponent.call(
this
);
this.addButton(_('Save'), this.onSaveClick, this);
this.addEvents({
watchdiredit: true,
});
},
show: function(watchdir_id, options) {
Deluge.ux.AutoAdd.EditAutoAddCommandWindow.superclass.show.call(
this
);
this.watchdir_id = watchdir_id;
this.loadOptions(options);
},
onSaveClick: function() {
try {
var options = this.getOptions();
deluge.client.autoadd.set_options(this.watchdir_id, options, {
success: function() {
this.fireEvent('watchdiredit', this, options);
},
scope: this,
});
} catch (err) {
Ext.Msg.show({
title: _('Incompatible Option'),
msg: err,
buttons: Ext.Msg.OK,
scope: this,
});
}
this.hide();
},
}
);
/**
* @class Deluge.ux.AutoAdd.AddAutoAddCommandWindow
* @extends Deluge.ux.AutoAdd.AutoAddWindowBase
*/
Deluge.ux.AutoAdd.AddAutoAddCommandWindow = Ext.extend(
Deluge.ux.AutoAdd.AutoAddWindowBase,
{
title: _('Add Watch Folder'),
initComponent: function() {
Deluge.ux.AutoAdd.AddAutoAddCommandWindow.superclass.initComponent.call(
this
);
this.addButton(_('Add'), this.onAddClick, this);
this.addEvents({
watchdiradd: true,
});
},
show: function() {
Deluge.ux.AutoAdd.AddAutoAddCommandWindow.superclass.show.call(
this
);
this.loadOptions();
},
onAddClick: function() {
var options = this.getOptions();
deluge.client.autoadd.add(options, {
success: function() {
this.fireEvent('watchdiradd', this, options);
this.hide();
},
failure: function(err) {
const regex = /: (.*\n)\n?\]/m;
var error;
if ((error = regex.exec(err.error.message)) !== null) {
error = error[1];
} else {
error = err.error.message;
}
Ext.Msg.show({
title: _('Incompatible Option'),
msg: error,
buttons: Ext.Msg.OK,
scope: this,
});
},
scope: this,
});
},
}
);

View File

@ -0,0 +1,304 @@
/**
* Script: main_tab.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.
*/
Ext.ns('Deluge.ux.AutoAdd');
/**
* @class Deluge.ux.AutoAdd.AutoAddMainPanel
* @extends Ext.Panel
*/
Deluge.ux.AutoAdd.AutoAddMainPanel = Ext.extend(Ext.Panel, {
id: 'main_tab_panel',
title: _('Main'),
initComponent: function() {
Deluge.ux.AutoAdd.AutoAddMainPanel.superclass.initComponent.call(this);
this.watchFolderFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Watch Folder'),
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
width: '85%',
labelWidth: 1,
items: [
{
xtype: 'textfield',
id: 'path',
hideLabel: true,
width: 304,
},
{
hideLabel: true,
id: 'enabled',
xtype: 'checkbox',
boxLabel: _('Enable this watch folder'),
checked: true,
},
],
});
this.torrentActionFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Torrent File Action'),
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
width: '85%',
labelWidth: 1,
defaults: {
style: 'margin-bottom: 2px',
},
items: [
{
xtype: 'radiogroup',
columns: 1,
items: [
{
xtype: 'radio',
name: 'torrent_action',
id: 'isnt_append_extension',
boxLabel: _('Delete .torrent after adding'),
checked: true,
hideLabel: true,
listeners: {
check: function(cb, newValue) {
if (newValue) {
Ext.getCmp(
'append_extension'
).setDisabled(newValue);
Ext.getCmp('copy_torrent').setDisabled(
newValue
);
Ext.getCmp(
'delete_copy_torrent_toggle'
).setDisabled(newValue);
}
},
},
},
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
xtype: 'radio',
name: 'torrent_action',
id: 'append_extension_toggle',
boxLabel: _(
'Append extension after adding:'
),
hideLabel: true,
listeners: {
check: function(cb, newValue) {
if (newValue) {
Ext.getCmp(
'append_extension'
).setDisabled(!newValue);
Ext.getCmp(
'copy_torrent'
).setDisabled(newValue);
Ext.getCmp(
'delete_copy_torrent_toggle'
).setDisabled(newValue);
}
},
},
},
{
xtype: 'textfield',
id: 'append_extension',
hideLabel: true,
disabled: true,
style: 'margin-left: 2px',
width: 112,
},
],
},
{
xtype: 'container',
hideLabel: true,
items: [
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
xtype: 'radio',
name: 'torrent_action',
id: 'copy_torrent_toggle',
boxLabel: _(
'Copy of .torrent files to:'
),
hideLabel: true,
listeners: {
check: function(cb, newValue) {
if (newValue) {
Ext.getCmp(
'append_extension'
).setDisabled(newValue);
Ext.getCmp(
'copy_torrent'
).setDisabled(
!newValue
);
Ext.getCmp(
'delete_copy_torrent_toggle'
).setDisabled(
!newValue
);
}
},
},
},
{
xtype: 'textfield',
id: 'copy_torrent',
hideLabel: true,
disabled: true,
style: 'margin-left: 2px',
width: 152,
},
],
},
{
xtype: 'checkbox',
id: 'delete_copy_torrent_toggle',
boxLabel: _(
'Delete copy of torrent file on remove'
),
style: 'margin-left: 10px',
disabled: true,
},
],
},
],
},
],
});
this.downloadFolderFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Download Folder'),
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
width: '85%',
labelWidth: 1,
items: [
{
hideLabel: true,
id: 'download_location_toggle',
xtype: 'checkbox',
boxLabel: _('Set download folder'),
listeners: {
check: function(cb, checked) {
Ext.getCmp('download_location').setDisabled(
!checked
);
},
},
},
{
xtype: 'textfield',
id: 'download_location',
hideLabel: true,
width: 304,
disabled: true,
},
],
});
this.moveCompletedFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Move Completed'),
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
width: '85%',
labelWidth: 1,
items: [
{
hideLabel: true,
id: 'move_completed_toggle',
xtype: 'checkbox',
boxLabel: _('Set move completed folder'),
listeners: {
check: function(cb, checked) {
Ext.getCmp('move_completed_path').setDisabled(
!checked
);
},
},
},
{
xtype: 'textfield',
id: 'move_completed_path',
hideLabel: true,
width: 304,
disabled: true,
},
],
});
this.LabelFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Label'),
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 3px;',
//width: '85%',
labelWidth: 1,
//hidden: true,
items: [
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
hashLabel: false,
id: 'label_toggle',
xtype: 'checkbox',
boxLabel: _('Label:'),
listeners: {
check: function(cb, checked) {
Ext.getCmp('label').setDisabled(!checked);
},
},
},
{
xtype: 'combo',
id: 'label',
hideLabel: true,
//width: 220,
width: 254,
disabled: true,
style: 'margin-left: 2px',
mode: 'local',
valueField: 'displayText',
displayField: 'displayText',
},
],
},
],
});
this.add([
this.watchFolderFset,
this.torrentActionFset,
this.downloadFolderFset,
this.moveCompletedFset,
this.LabelFset,
]);
},
});

View File

@ -0,0 +1,302 @@
/**
* Script: options_tab.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.
*/
Ext.ns('Deluge.ux.AutoAdd');
/**
* @class Deluge.ux.AutoAdd.AutoAddOptionsPanel
* @extends Ext.Panel
*/
Deluge.ux.AutoAdd.AutoAddOptionsPanel = Ext.extend(Ext.Panel, {
id: 'options_tab_panel',
title: _('Options'),
initComponent: function() {
Deluge.ux.AutoAdd.AutoAddOptionsPanel.superclass.initComponent.call(
this
);
var maxDownload = {
idCheckbox: 'max_download_speed_toggle',
labelCheckbox: 'Max Download Speed (KiB/s):',
idSpinner: 'max_download_speed',
decimalPrecision: 1,
};
var maxUploadSpeed = {
idCheckbox: 'max_upload_speed_toggle',
labelCheckbox: 'Max upload Speed (KiB/s):',
idSpinner: 'max_upload_speed',
decimalPrecision: 1,
};
var maxConnections = {
idCheckbox: 'max_connections_toggle',
labelCheckbox: 'Max Connections::',
idSpinner: 'max_connections',
decimalPrecision: 0,
};
var maxUploadSlots = {
idCheckbox: 'max_upload_slots_toggle',
labelCheckbox: 'Max Upload Slots:',
idSpinner: 'max_upload_slots',
decimalPrecision: 0,
};
// queue data
var addPause = {
idCheckbox: 'add_paused_toggle',
labelCheckbox: 'Add Pause:',
nameRadio: 'add_paused',
labelRadio: {
yes: 'Yes',
no: 'No',
},
};
var queueTo = {
idCheckbox: 'queue_to_top_toggle',
labelCheckbox: 'Queue To:',
nameRadio: 'queue_to_top',
labelRadio: {
yes: 'Top',
no: 'Bottom',
},
};
var autoManaged = {
idCheckbox: 'auto_managed_toggle',
labelCheckbox: 'Auto Managed:',
nameRadio: 'auto_managed',
labelRadio: {
yes: 'Yes',
no: 'No',
},
};
this.ownerFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Owner'),
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
//width: '85%',
labelWidth: 1,
items: [
{
xtype: 'combo',
id: 'owner',
hideLabel: true,
width: 312,
mode: 'local',
valueField: 'displayText',
displayField: 'displayText',
},
],
});
this.bandwidthFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Bandwidth'),
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
//width: '85%',
labelWidth: 1,
defaults: {
style: 'margin-bottom: 5px',
},
});
this.bandwidthFset.add(this._getBandwidthContainer(maxDownload));
this.bandwidthFset.add(this._getBandwidthContainer(maxUploadSpeed));
this.bandwidthFset.add(this._getBandwidthContainer(maxConnections));
this.bandwidthFset.add(this._getBandwidthContainer(maxUploadSlots));
this.queueFset = new Ext.form.FieldSet({
xtype: 'fieldset',
border: false,
title: _('Queue'),
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
//width: '85%',
labelWidth: 1,
defaults: {
style: 'margin-bottom: 5px',
},
items: [
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
},
],
});
this.queueFset.add(this._getQueueContainer(addPause));
this.queueFset.add(this._getQueueContainer(queueTo));
this.queueFset.add(this._getQueueContainer(autoManaged));
this.queueFset.add({
xtype: 'container',
hideLabel: true,
items: [
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
xtype: 'checkbox',
id: 'stop_at_ratio_toggle',
boxLabel: _('Stop seed at ratio:'),
hideLabel: true,
width: 175,
listeners: {
check: function(cb, checked) {
Ext.getCmp('stop_ratio').setDisabled(
!checked
);
Ext.getCmp('remove_at_ratio').setDisabled(
!checked
);
},
},
},
{
xtype: 'spinnerfield',
id: 'stop_ratio',
hideLabel: true,
disabled: true,
value: 0.0,
minValue: 0.0,
maxValue: 100.0,
decimalPrecision: 1,
incrementValue: 0.1,
style: 'margin-left: 2px',
width: 100,
},
],
},
{
xtype: 'container',
layout: 'hbox',
hideLabel: true,
style: 'margin-left: 10px',
items: [
{
xtype: 'checkbox',
id: 'remove_at_ratio',
boxLabel: _('Remove at ratio'),
disabled: true,
checked: true,
},
{
xtype: 'checkbox',
id: 'remove_at_ratio_toggle',
disabled: true,
checked: true,
hidden: true,
},
{
xtype: 'checkbox',
id: 'stop_ratio_toggle',
disabled: true,
checked: true,
hidden: true,
},
{
xtype: 'checkbox',
id: 'stop_ratio_toggle',
disabled: true,
checked: true,
hidden: true,
},
],
},
],
});
this.queueFset.add({
xtype: 'checkbox',
id: 'seed_mode',
boxLabel: _('Skip File Hash Check'),
hideLabel: true,
width: 175,
});
this.add([this.ownerFset, this.bandwidthFset, this.queueFset]);
},
_getBandwidthContainer: function(values) {
return new Ext.Container({
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
xtype: 'checkbox',
hideLabel: true,
id: values.idCheckbox,
boxLabel: _(values.labelCheckbox),
width: 175,
listeners: {
check: function(cb, checked) {
Ext.getCmp(values.idSpinner).setDisabled(!checked);
},
},
},
{
xtype: 'spinnerfield',
id: values.idSpinner,
hideLabel: true,
disabled: true,
minValue: -1,
maxValue: 10000,
value: 0.0,
decimalPrecision: values.decimalPrecision,
style: 'margin-left: 2px',
width: 100,
},
],
});
},
_getQueueContainer: function(values) {
return new Ext.Container({
xtype: 'container',
layout: 'hbox',
hideLabel: true,
items: [
{
xtype: 'checkbox',
hideLabel: true,
id: values.idCheckbox,
boxLabel: _(values.labelCheckbox),
width: 175,
listeners: {
check: function(cb, checked) {
Ext.getCmp(values.nameRadio).setDisabled(!checked);
Ext.getCmp('not_' + values.nameRadio).setDisabled(
!checked
);
},
},
},
{
xtype: 'radio',
name: values.nameRadio,
id: values.nameRadio,
boxLabel: _(values.labelRadio.yes),
hideLabel: true,
checked: true,
disabled: true,
width: 50,
},
{
xtype: 'radio',
name: values.nameRadio,
id: 'not_' + values.nameRadio,
boxLabel: _(values.labelRadio.no),
hideLabel: true,
disabled: true,
},
],
});
},
});

View File

@ -1,116 +1,134 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="prefs_window">
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="GtkWindow" id="prefs_window">
<property name="can_focus">False</property>
<child>
<widget class="GtkHBox" id="hbox9">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="hbox9">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
<child>
<widget class="GtkAlignment" id="prefs_box_1">
<object class="GtkAlignment" id="prefs_box_1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkVBox" id="prefs_box">
<object class="GtkBox" id="prefs_box">
<property name="width_request">340</property>
<property name="height_request">390</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">3</property>
<property name="orientation">vertical</property>
<signal name="parent_set" handler="on_parent_set"/>
<child>
<widget class="GtkFrame" id="frame1">
<object class="GtkFrame" id="frame1">
<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="GtkVBox" id="watchdirs_vbox">
<object class="GtkBox" id="watchdirs_vbox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</widget>
</object>
</child>
<child>
<widget class="GtkLabel" id="label1">
<child type="label">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Watch Folders:&lt;/b&gt;</property>
<property name="use_markup">True</property>
</widget>
<packing>
<property name="type">label_item</property>
</packing>
</object>
</child>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkHButtonBox" id="hbuttonbox1">
<object class="GtkHButtonBox" id="hbuttonbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkButton" id="add_button">
<property name="label" translatable="no">gtk-add</property>
<object class="GtkButton" id="add_button">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_add_button_clicked"/>
</widget>
<signal name="clicked" handler="on_add_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="remove_button">
<property name="label" translatable="no">gtk-remove</property>
<object class="GtkButton" id="remove_button">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_remove_button_clicked"/>
</widget>
<signal name="clicked" handler="on_remove_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="edit_button">
<property name="label" translatable="no">gtk-edit</property>
<object class="GtkButton" id="edit_button">
<property name="label">gtk-edit</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_edit_button_clicked"/>
</widget>
<signal name="clicked" handler="on_edit_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</glade-interface>
</object>
</interface>

View File

@ -0,0 +1,580 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# 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>
#
# 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 gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402
# isort:imports-thirdparty
from gi.repository import Gtk
# isort:imports-firstparty
import deluge.common
import deluge.component as component
from deluge.plugins.pluginbase import Gtk3PluginBase
from deluge.ui.client import client
from deluge.ui.gtk3 import dialogs
# isort:imports-localfolder
from .common import get_resource
log = logging.getLogger(__name__)
class IncompatibleOption(Exception):
pass
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=None, watchdir_id=None):
if options is None:
options = {}
self.builder = Gtk.Builder()
self.builder.add_from_file(get_resource('autoadd_options.ui'))
self.builder.connect_signals(
{
'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.builder.get_object('options_dialog')
self.dialog.set_transient_for(component.get('Preferences').pref_dialog)
if watchdir_id:
# We have an existing watchdir_id, we are editing
self.builder.get_object('opts_add_button').hide()
self.builder.get_object('opts_apply_button').show()
self.watchdir_id = watchdir_id
else:
# We don't have an id, adding
self.builder.get_object('opts_add_button').show()
self.builder.get_object('opts_apply_button').hide()
self.watchdir_id = None
self.load_options(options)
self.dialog.run()
def load_options(self, options):
self.builder.get_object('enabled').set_active(options.get('enabled', True))
self.builder.get_object('append_extension_toggle').set_active(
options.get('append_extension_toggle', False)
)
self.builder.get_object('append_extension').set_text(
options.get('append_extension', '.added')
)
self.builder.get_object('download_location_toggle').set_active(
options.get('download_location_toggle', False)
)
self.builder.get_object('copy_torrent_toggle').set_active(
options.get('copy_torrent_toggle', False)
)
self.builder.get_object('delete_copy_torrent_toggle').set_active(
options.get('delete_copy_torrent_toggle', False)
)
self.builder.get_object('seed_mode').set_active(options.get('seed_mode', False))
self.accounts.clear()
self.labels.clear()
combobox = self.builder.get_object('OwnerCombobox')
combobox_render = Gtk.CellRendererText()
combobox.pack_start(combobox_render, True)
combobox.add_attribute(combobox_render, 'text', 0)
combobox.set_model(self.accounts)
label_widget = self.builder.get_object('label')
label_widget.get_child().set_text(options.get('label', ''))
label_widget.set_model(self.labels)
label_widget.set_entry_text_column(0)
self.builder.get_object('label_toggle').set_active(
options.get('label_toggle', False)
)
for spin_id in self.spin_ids + self.spin_int_ids:
self.builder.get_object(spin_id).set_value(options.get(spin_id, 0))
self.builder.get_object(spin_id + '_toggle').set_active(
options.get(spin_id + '_toggle', False)
)
for chk_id in self.chk_ids:
self.builder.get_object(chk_id).set_active(bool(options.get(chk_id, True)))
self.builder.get_object(chk_id + '_toggle').set_active(
options.get(chk_id + '_toggle', False)
)
if not options.get('add_paused', True):
self.builder.get_object('isnt_add_paused').set_active(True)
if not options.get('queue_to_top', True):
self.builder.get_object('isnt_queue_to_top').set_active(True)
if not options.get('auto_managed', True):
self.builder.get_object('isnt_auto_managed').set_active(True)
for field in [
'move_completed_path',
'path',
'download_location',
'copy_torrent',
]:
if client.is_localhost():
self.builder.get_object(field + '_chooser').set_current_folder(
options.get(field, os.path.expanduser('~'))
)
self.builder.get_object(field + '_chooser').show()
self.builder.get_object(field + '_entry').hide()
else:
self.builder.get_object(field + '_entry').set_text(
options.get(field, '')
)
self.builder.get_object(field + '_entry').show()
self.builder.get_object(field + '_chooser').hide()
self.set_sensitive()
def on_core_config(config):
if client.is_localhost():
self.builder.get_object('download_location_chooser').set_current_folder(
options.get('download_location', config['download_location'])
)
if options.get('move_completed_toggle', config['move_completed']):
self.builder.get_object('move_completed_toggle').set_active(True)
self.builder.get_object(
'move_completed_path_chooser'
).set_current_folder(
options.get(
'move_completed_path', config['move_completed_path']
)
)
if options.get('copy_torrent_toggle', config['copy_torrent_file']):
self.builder.get_object('copy_torrent_toggle').set_active(True)
self.builder.get_object('copy_torrent_chooser').set_current_folder(
options.get('copy_torrent', config['torrentfiles_location'])
)
else:
self.builder.get_object('download_location_entry').set_text(
options.get('download_location', config['download_location'])
)
if options.get('move_completed_toggle', config['move_completed']):
self.builder.get_object('move_completed_toggle').set_active(
options.get('move_completed_toggle', False)
)
self.builder.get_object('move_completed_path_entry').set_text(
options.get(
'move_completed_path', config['move_completed_path']
)
)
if options.get('copy_torrent_toggle', config['copy_torrent_file']):
self.builder.get_object('copy_torrent_toggle').set_active(True)
self.builder.get_object('copy_torrent_entry').set_text(
options.get('copy_torrent', config['torrentfiles_location'])
)
if options.get(
'delete_copy_torrent_toggle', config['del_copy_torrent_file']
):
self.builder.get_object('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')
selected_iter = None
for account in accounts:
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, account['username'])
if account['username'] == owner:
selected_iter = acc_iter
self.builder.get_object('OwnerCombobox').set_active_iter(selected_iter)
def on_accounts_failure(failure):
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.builder.get_object('OwnerCombobox').set_active(0)
self.builder.get_object('OwnerCombobox').set_sensitive(False)
def on_labels(labels):
log.debug('Got Labels: %s', labels)
for label in labels:
self.labels.set_value(self.labels.append(), 0, label)
label_widget = self.builder.get_object('label')
label_widget.set_model(self.labels)
label_widget.set_entry_text_column(0)
def on_failure(failure):
log.exception(failure)
def on_get_enabled_plugins(result):
if 'Label' in result:
self.builder.get_object('label_frame').show()
client.label.get_labels().addCallback(on_labels).addErrback(on_failure)
else:
self.builder.get_object('label_frame').hide()
self.builder.get_object('label_toggle').set_active(False)
client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins)
if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN:
client.core.get_known_accounts().addCallback(
on_accounts, options.get('owner', client.get_auth_user())
).addErrback(on_accounts_failure)
else:
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, client.get_auth_user())
self.builder.get_object('OwnerCombobox').set_active(0)
self.builder.get_object('OwnerCombobox').set_sensitive(False)
def set_sensitive(self):
maintoggles = [
'download_location',
'append_extension',
'move_completed',
'label',
'max_download_speed',
'max_upload_speed',
'max_connections',
'max_upload_slots',
'add_paused',
'auto_managed',
'stop_at_ratio',
'queue_to_top',
'copy_torrent',
]
for maintoggle in maintoggles:
self.on_toggle_toggled(self.builder.get_object(maintoggle + '_toggle'))
def on_toggle_toggled(self, tb):
toggle = tb.get_name().replace('_toggle', '')
isactive = tb.get_active()
if toggle == 'download_location':
self.builder.get_object('download_location_chooser').set_sensitive(isactive)
self.builder.get_object('download_location_entry').set_sensitive(isactive)
elif toggle == 'append_extension':
self.builder.get_object('append_extension').set_sensitive(isactive)
elif toggle == 'copy_torrent':
self.builder.get_object('copy_torrent_entry').set_sensitive(isactive)
self.builder.get_object('copy_torrent_chooser').set_sensitive(isactive)
self.builder.get_object('delete_copy_torrent_toggle').set_sensitive(
isactive
)
elif toggle == 'move_completed':
self.builder.get_object('move_completed_path_chooser').set_sensitive(
isactive
)
self.builder.get_object('move_completed_path_entry').set_sensitive(isactive)
self.builder.get_object('move_completed').set_active(isactive)
elif toggle == 'label':
self.builder.get_object('label').set_sensitive(isactive)
elif toggle == 'max_download_speed':
self.builder.get_object('max_download_speed').set_sensitive(isactive)
elif toggle == 'max_upload_speed':
self.builder.get_object('max_upload_speed').set_sensitive(isactive)
elif toggle == 'max_connections':
self.builder.get_object('max_connections').set_sensitive(isactive)
elif toggle == 'max_upload_slots':
self.builder.get_object('max_upload_slots').set_sensitive(isactive)
elif toggle == 'add_paused':
self.builder.get_object('add_paused').set_sensitive(isactive)
self.builder.get_object('isnt_add_paused').set_sensitive(isactive)
elif toggle == 'queue_to_top':
self.builder.get_object('queue_to_top').set_sensitive(isactive)
self.builder.get_object('isnt_queue_to_top').set_sensitive(isactive)
elif toggle == 'auto_managed':
self.builder.get_object('auto_managed').set_sensitive(isactive)
self.builder.get_object('isnt_auto_managed').set_sensitive(isactive)
elif toggle == 'stop_at_ratio':
self.builder.get_object('remove_at_ratio_toggle').set_active(isactive)
self.builder.get_object('stop_ratio_toggle').set_active(isactive)
self.builder.get_object('stop_at_ratio').set_active(isactive)
self.builder.get_object('stop_ratio').set_sensitive(isactive)
self.builder.get_object('remove_at_ratio').set_sensitive(isactive)
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 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)
result.cleanFailure()
d.run()
def on_added(self, result):
self.dialog.destroy()
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 as ex:
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
def on_cancel(self, event=None):
self.dialog.destroy()
def generate_opts(self):
# generate options dict based on gtk objects
options = {}
options['enabled'] = self.builder.get_object('enabled').get_active()
if client.is_localhost():
options['path'] = self.builder.get_object('path_chooser').get_filename()
options['download_location'] = self.builder.get_object(
'download_location_chooser'
).get_filename()
options['move_completed_path'] = self.builder.get_object(
'move_completed_path_chooser'
).get_filename()
options['copy_torrent'] = self.builder.get_object(
'copy_torrent_chooser'
).get_filename()
else:
options['path'] = self.builder.get_object('path_entry').get_text()
options['download_location'] = self.builder.get_object(
'download_location_entry'
).get_text()
options['move_completed_path'] = self.builder.get_object(
'move_completed_path_entry'
).get_text()
options['copy_torrent'] = self.builder.get_object(
'copy_torrent_entry'
).get_text()
options['label'] = (
self.builder.get_object('label').get_child().get_text().lower()
)
options['append_extension'] = self.builder.get_object(
'append_extension'
).get_text()
options['owner'] = self.accounts[
self.builder.get_object('OwnerCombobox').get_active()
][0]
for key in [
'append_extension_toggle',
'download_location_toggle',
'label_toggle',
'copy_torrent_toggle',
'delete_copy_torrent_toggle',
'seed_mode',
]:
options[key] = self.builder.get_object(key).get_active()
for spin_id in self.spin_ids:
options[spin_id] = self.builder.get_object(spin_id).get_value()
options[spin_id + '_toggle'] = self.builder.get_object(
spin_id + '_toggle'
).get_active()
for spin_int_id in self.spin_int_ids:
options[spin_int_id] = self.builder.get_object(
spin_int_id
).get_value_as_int()
options[spin_int_id + '_toggle'] = self.builder.get_object(
spin_int_id + '_toggle'
).get_active()
for chk_id in self.chk_ids:
options[chk_id] = self.builder.get_object(chk_id).get_active()
options[chk_id + '_toggle'] = self.builder.get_object(
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!'
)
)
return options
class GtkUI(Gtk3PluginBase):
def enable(self):
self.builder = Gtk.Builder()
self.builder.add_from_file(get_resource('config.ui'))
self.builder.connect_signals(self)
self.opts_dialog = OptionsDialog()
component.get('PluginManager').register_hook(
'on_apply_prefs', self.on_apply_prefs
)
component.get('PluginManager').register_hook(
'on_show_prefs', self.on_show_prefs
)
client.register_event_handler(
'AutoaddOptionsChangedEvent', self.on_options_changed_event
)
self.watchdirs = {}
vbox = self.builder.get_object('watchdirs_vbox')
sw = Gtk.ScrolledWindow()
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
vbox.pack_start(sw, True, True, 0)
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.set_rules_hint(True)
self.create_columns(self.treeView)
sw.add(self.treeView)
sw.show_all()
component.get('Preferences').add_page(
_('AutoAdd'), self.builder.get_object('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('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.items():
store.append(
[
watchdir_id,
watchdir['enabled'],
watchdir.get('owner', 'localclient'),
watchdir['path'],
]
)
return store
def create_columns(self, treeview):
renderer_toggle = Gtk.CellRendererToggle()
column = Gtk.TreeViewColumn(
_('Active'), renderer_toggle, activatable=1, active=1
)
column.set_sort_column_id(1)
treeview.append_column(column)
tt = Gtk.Tooltip()
tt.set_text(_('Double-click to toggle'))
treeview.set_tooltip_cell(tt, None, None, renderer_toggle)
renderertext = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(_('Owner'), renderertext, text=2)
column.set_sort_column_id(2)
treeview.append_column(column)
tt2 = Gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeview.set_has_tooltip(True)
renderertext = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(_('Path'), renderertext, text=3)
column.set_sort_column_id(3)
treeview.append_column(column)
tt2 = Gtk.Tooltip()
tt2.set_text(_('Double-click to edit'))
treeview.set_has_tooltip(True)
def load_watchdir_list(self):
pass
def add_watchdir_entry(self):
pass
def on_add_button_clicked(self, event=None):
# display options_window
self.opts_dialog.show()
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):
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 self.watchdirs[watchdir_id]['enabled']:
client.autoadd.disable_watchdir(watchdir_id)
else:
client.autoadd.enable_watchdir(watchdir_id)
else:
self.opts_dialog.show(self.watchdirs[watchdir_id], watchdir_id)
def on_listitem_activated(self, treeview):
tree, tree_id = self.treeView.get_selection().get_selected()
if tree_id:
self.builder.get_object('edit_button').set_sensitive(True)
self.builder.get_object('remove_button').set_sensitive(True)
else:
self.builder.get_object('edit_button').set_sensitive(False)
self.builder.get_object('remove_button').set_sensitive(False)
def on_apply_prefs(self):
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):
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
def on_options_changed_event(self):
client.autoadd.get_watchdirs().addCallback(self.cb_get_config)
def cb_get_config(self, watchdirs):
"""callback for on show_prefs"""
log.trace('Got whatchdirs from core: %s', watchdirs)
self.watchdirs = watchdirs or {}
self.store.clear()
for watchdir_id, watchdir in self.watchdirs.items():
self.store.append(
[
watchdir_id,
watchdir['enabled'],
watchdir.get('owner', 'localclient'),
watchdir['path'],
]
)
# Workaround for cached glade signal appearing when re-enabling plugin in same session
if self.builder.get_object('edit_button'):
# Disable the remove and edit buttons, because nothing in the store is selected
self.builder.get_object('remove_button').set_sensitive(False)
self.builder.get_object('edit_button').set_sensitive(False)

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
#
# 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>
#
# 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.plugins.pluginbase import WebPluginBase
from .common import get_resource
log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [
get_resource('autoadd.js'),
get_resource('autoadd_options.js'),
get_resource('main_tab.js', True),
get_resource('options_tab.js', True),
]
def enable(self):
pass
def disable(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.8'
__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_' + __plugin_name__.lower(): ['data/*', 'data/*/*']}
setup(
name=__plugin_name__,
@ -60,15 +35,14 @@ setup(
license=__license__,
long_description=__long_description__ if __long_description__ else __description__,
packages=find_packages(),
namespace_packages = ["deluge", "deluge.plugins"],
package_data = __pkg_data__,
package_data=__pkg_data__,
entry_points="""
[deluge.plugin.core]
%s = deluge.plugins.%s:CorePlugin
[deluge.plugin.gtkui]
%s = deluge.plugins.%s:GtkUIPlugin
%s = deluge_%s:CorePlugin
[deluge.plugin.gtk3ui]
%s = deluge_%s:Gtk3UIPlugin
[deluge.plugin.web]
%s = deluge.plugins.%s:WebUIPlugin
""" % ((__plugin_name__, __plugin_name__.lower())*3)
%s = deluge_%s:WebUIPlugin
"""
% ((__plugin_name__, __plugin_name__.lower()) * 3),
)

View File

@ -1,2 +0,0 @@
# this is a namespace package
__import__('pkg_resources').declare_namespace(__name__)

View File

@ -1,2 +0,0 @@
# this is a namespace package
__import__('pkg_resources').declare_namespace(__name__)

View File

@ -1,55 +0,0 @@
#
# blocklist/__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,187 +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
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))
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
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
"""
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
"""
try:
return func(self, *args, **kwargs)
except:
(value, tb) = exc_info()[1:]
raise error, value, tb
return wrapper
return decorator
def remove_zeros(ip):
"""
Removes unneeded zeros from ip addresses.
Example: 000.000.000.003 -> 0.0.0.3
:param ip: the ip address
:type ip: string
:returns: the ip address without the unneeded zeros
:rtype: string
"""
return ".".join([part.lstrip("0").zfill(1) for part in ip.split(".")])
class BadIP(Exception):
_message = None
def __init__(self, message):
self.message = message
def __set_message(self, message):
self._message = message
def __get_message(self):
return self._message
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
self.q3 = q3
self.q4 = q4
self._long = 0
for q in self.quadrants():
self._long = (self._long << 8) | int(q)
@property
def address(self):
return '.'.join([str(q) for q in [self.q1, self.q2, self.q3, self.q4]])
@property
def long(self):
return self._long
@classmethod
def parse(cls, ip):
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))
return cls(q1, q2, q3, q4)
def quadrants(self):
return (self.q1, self.q2, self.q3, self.q4)
# def next_ip(self):
# (q1, q2, q3, q4) = self.quadrants()
# if q4 >= 255:
# if q3 >= 255:
# if q2 >= 255:
# if q1 >= 255:
# raise BadIP(_("There ain't a next IP address"))
# q1 += 1
# else:
# q2 += 1
# else:
# q3 += 1
# else:
# q4 += 1
# return IP(q1, q2, q3, q4)
#
# def previous_ip(self):
# (q1, q2, q3, q4) = self.quadrants()
# if q4 <= 1:
# if q3 <= 1:
# if q2 <= 1:
# if q1 <= 1:
# raise BadIP(_("There ain't a previous IP address"))
# q1 -= 1
# else:
# q2 -= 1
# else:
# q3 -= 1
# else:
# q4 -= 1
# return IP(q1, q2, q3, q4)
def __lt__(self, other):
if isinstance(other, basestring):
other = IP.parse(other)
return self.long < other.long
def __gt__(self, other):
if isinstance(other, basestring):
other = IP.parse(other)
return self.long > other.long
def __eq__(self, other):
if isinstance(other, basestring):
other = IP.parse(other)
return self.long == other.long
def __repr__(self):
return '<%s long=%s address="%s">' % (
self.__class__.__name__, self.long, self.address
)

View File

@ -1,531 +0,0 @@
#
# core.py
#
# 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.
#
#
import os
import time
import logging
from datetime import datetime, timedelta
from email.utils import formatdate
from urlparse import urljoin
import shutil
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
# TODO: check return values for deferred callbacks
# TODO: review class attributes for redundancy
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": [],
}
# Constants
ALLOW_RANGE = 0
BLOCK_RANGE = 1
class Core(CorePluginBase):
def enable(self):
log.debug('Blocklist: Plugin enabled...')
self.is_url = True
self.is_downloading = False
self.is_importing = False
self.has_imported = False
self.up_to_date = False
self.need_to_resume_session = False
self.num_whited = 0
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.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
update_now = False
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():
update_now = True
else:
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)
# 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:
self.update_timer.start(
self.config["check_after_days"] * 24 * 60 * 60, update_now
)
def disable(self):
self.config.save()
log.debug("Reset IP filter")
self.core.session.get_ip_filter().add_rule(
"0.0.0.0", "255.255.255.255", ALLOW_RANGE
)
log.debug('Blocklist: Plugin disabled')
def update(self):
pass
## Exported RPC methods ###
@export
def check_import(self, force=False):
"""
Imports latest blocklist specified by blocklist url
Only downloads/imports if necessary or forced
:param force: optional argument to force download/import
:type force: boolean
:returns: a Deferred which fires when the blocklist has been imported
:rtype: Deferred
"""
# Reset variables
self.filename = None
self.force_download = force
self.failed_attempts = 0
self.auto_detected = False
self.up_to_date = False
if force:
self.reader = None
self.is_url = is_url(self.config["url"])
# Start callback chain
if self.is_url:
d = self.download_list()
d.addCallbacks(self.on_download_complete, self.on_download_error)
d.addCallback(self.import_list)
else:
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)
return d
@export
def get_config(self):
"""
Returns 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'
:param config: config to set
:type config: dictionary
"""
needs_blocklist_import = False
for key in config.keys():
if key == 'whitelisted':
saved = set(self.config[key])
update = set(config[key])
diff = saved.symmetric_difference(update)
if diff:
log.debug("Whitelist changed. Updating...")
added = update.intersection(diff)
removed = saved.intersection(diff)
if added:
for ip in added:
try:
ip = IP.parse(ip)
self.blocklist.add_rule(
ip.address, ip.address, ALLOW_RANGE
)
saved.add(ip.address)
log.debug("Added %s to whitelisted", ip)
self.num_whited += 1
except BadIP, e:
log.error("Bad IP: %s", e)
continue
if removed:
needs_blocklist_import = True
for ip in removed:
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)
continue
self.config[key] = list(saved)
continue
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():
update_now = True
self.update_timer.running and 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
)
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.")
self.has_imported = False
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
:returns: the status dict of the plugin
:rtype: dict
"""
status = {}
if self.is_downloading:
status["state"] = "Downloading"
elif self.is_importing:
status["state"] = "Importing"
else:
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"]
return status
####
def update_info(self, blocklist):
"""
Updates blocklist info
: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)
self.filename = blocklist
return blocklist
def download_list(self, url=None):
"""
Downloads the blocklist specified by 'url' in the config
: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
if fp > 1.0:
fp = 1.0
else:
fp = 0.0
self.file_progress = fp
import socket
socket.setdefaulttimeout(self.config["timeout"])
if not 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)
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"),
on_retrieve_data, headers
)
def on_download_complete(self, blocklist):
"""
Runs any download clean up functions
: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)
self.is_downloading = False
return threads.deferToThread(self.update_info, blocklist)
def on_download_error(self, f):
"""
Recovers from download error
: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
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!")
self.up_to_date = True
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")
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
: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")
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)
self.blocklist.add_rule(start.address, end.address, BLOCK_RANGE)
self.num_blocked += 1
def on_finish_read(result):
"""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"]:
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)
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")
return defer.succeed(blocklist)
self.is_importing = True
self.num_blocked = 0
self.num_whited = 0
self.blocklist = self.core.session.get_ip_filter()
if not blocklist:
blocklist = self.filename
if not self.reader:
self.auto_detect(blocklist)
self.auto_detected = True
def on_reader_failure(failure):
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)
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
: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")
d = blocklist
self.is_importing = False
self.has_imported = True
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)
d = threads.deferToThread(shutil.move, blocklist, cache)
else:
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
: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)
d = f
self.is_importing = False
try_again = False
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")
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())
blocklist = cache
try_again = True
if try_again:
d = self.import_list(blocklist)
d.addCallbacks(self.on_import_complete, self.on_import_error)
return d
def auto_detect(self, blocklist):
"""
Tries to auto-detect the blocklist type
: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"] = ""
raise UnknownFormatError
else:
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
def resume_session(self, result):
self.core.session.resume()
self.need_to_resume_session = False
return result

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