Compare commits

...

366 Commits

Author SHA1 Message Date
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
934 changed files with 141409 additions and 128233 deletions

1
.gitattributes vendored
View File

@ -2,3 +2,4 @@
.gitmodules export-ignore
.gitignore export-ignore
*.py diff=python
ext-all.js diff=minjs

11
.gitignore vendored
View File

@ -2,18 +2,23 @@
build
.cache
dist
docs/source/modules
*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:
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

View File

@ -69,7 +69,7 @@ confidence=
# Arranged by category and use symbolic names instead of ids.
disable=
# Convention
missing-docstring, invalid-name,
missing-docstring, invalid-name, bad-continuation,
# Error
no-member, no-name-in-module,
# Information

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: .

View File

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

61
CHANGELOG.md Normal file
View File

@ -0,0 +1,61 @@
# Changelog
## 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.
### GtkUI
- 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
### WebUI
- Server (deluge-web) now daemonizes by default, use '-d' or '--do-not-daemonize' to disable.
- Fixed the '--base' option to work for regular use, not just with reverse proxies.
### Blocklist Plugin
- 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.

View File

@ -1,50 +0,0 @@
=== Deluge 2.0 (In Development) ===
* Improved Logging
* Removed the AutoAdd feature on the core. It's now handled with the AutoAdd
plugin, which is also shipped with Deluge, and it does a better job and
now, it even supports multiple users perfectly.
* Authentication/Permission exceptions are now sent to clients and recreated
there to allow acting upon them.
* Enforced the use of the "deluge.plugins" namespace to reduce package
names clashing beetween regular packages and deluge plugins.
==== Core ====
* Make the distinction between adding to the session new unmanaged torrents
and torrents loaded from state. This will break backwards compatability.
* Pass a copy of an event instead of passing the event arguments to the
event handlers. This will break backwards compatability.
* Allow changing ownership of torrents.
* File modifications on the auth file are now detected and when they happen,
the file is reloaded. Upon finding an old auth file with an old format, an
upgrade to the new format is made, file saved, and reloaded.
* Authentication no longer requires a username/password. If one or both of
these is missing, an authentication error will be sent to the client
which sould then ask the username/password to the user.
* Implemented sequential downloads.
* Provide information about a torrent's pieces states
==== GtkUI ====
* Allow changing ownership of torrents.
* Host entries in the Connection Manager UI are now editable.
* Implemented sequential downloads UI handling.
* Add optional pieces bar instead of a regular progress bar in torrent status tab.
* Make torrent opening compatible with all unicode paths.
* Fix magnet association button on Windows.
* Add keyboard shortcuts for changing queue position:
- Up: Ctrl+Alt+Up
- Down: Ctrl+Alt+Down
- Top: Ctrl+Alt+Shift+Up
- Bottom: Ctrl+Alt+Shift+Down
==== WebUI ====
* Server (deluge-web) now daemonizes by default, use '-d' or '--do-not-daemonize' to disable.
* Fixed the '--base' option to work for regular use, not just with reverse proxies.
==== Blocklist Plugin ====
* Implemented whitelist support to both core and GTK UI.
* Implemented ip filter cleaning before each update. Restarting the deluge
daemon is no longer needed.
* If "check_after_days" is 0(zero), the timer is not started anymore. It
would keep updating one call after the other. If the value changed, the
timer is now stopped and restarted using the new value.

29
DEPENDS
View File

@ -1,29 +0,0 @@
=== Core ===
* libtorrent (rasterbar) >= 1.1.1
* python >= 2.7.7
* setuptools
* twisted >= 11.1
* pyopenssl
* pyxdg
* chardet
* gettext
* python-geoip (optional)
* geoip-database (optional)
* setproctitle (optional)
* pillow (optional)
* py2-ipaddress (optional, required for Windows IPv6)
* rencode >= 1.0.2 (optional), python port bundled.
=== Gtk UI ===
* pygtk >= 2.16
* librsvg
* xdg-utils
* intltool
* python-notify (optional)
* pygame (optional)
* python-appindicator (optional)
=== Web UI ===
* mako
* slimit (optional), minifies JS files.

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,19 +1,24 @@
include AUTHORS ChangeLog DEPENDS LICENSE RELEASE-VERSION README.rst
include msgfmt.py minify_web_js.py version.py
exclude setup.cfg
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
include deluge/i18n/*.po
recursive-exclude deluge/i18n LC_MESSAGES *.mo
recursive-exclude deluge/i18n *.mo
graft deluge/plugins
recursive-exclude deluge/plugins create_dev_link.sh *.pyc *.egg
prune deluge/plugins/*/build
prune deluge/plugins/*/*.egg-info
graft deluge/tests/data
graft deluge/tests/twisted
graft deluge/tests/
recursive-exclude deluge/tests *.pyc
graft deluge/ui/data
recursive-exclude deluge/ui/data *.desktop *.xml

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

View File

@ -1,68 +0,0 @@
=========================
Deluge BitTorrent Client
=========================
|build-status| |docs|
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
.. |build-status| image:: https://travis-ci.org/deluge-torrent/deluge.svg
:target: https://travis-ci.org/deluge-torrent/deluge
.. |docs| image:: https://readthedocs.org/projects/deluge/badge/?version=develop
:target: https://readthedocs.org/projects/deluge/?badge=develop
:alt: Documentation Status

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

View File

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

View File

@ -25,6 +25,9 @@ except ImportError:
import libtorrent as 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))
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
raise ImportError(
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
)

View File

@ -86,15 +86,16 @@ argparse.ArgumentParser.find_subcommand = find_subcommand
argparse.ArgumentParser.set_default_subparser = set_default_subparser
def get_version():
def _get_version_detail():
version_str = '%s\n' % (common.get_version())
try:
from deluge._libtorrent import lt
version_str += 'libtorrent: %s\n' % lt.__version__
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(), ' '.join(common.get_os_version()))
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
return version_str
@ -141,7 +142,6 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
class HelpAction(argparse._HelpAction):
def __call__(self, parser, namespace, values, option_string=None):
if hasattr(parser, 'subparser'):
subparser = getattr(parser, 'subparser')
@ -151,11 +151,12 @@ class HelpAction(argparse._HelpAction):
parser.exit()
class BaseArgParser(argparse.ArgumentParser):
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['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)
@ -164,32 +165,73 @@ class BaseArgParser(argparse.ArgumentParser):
self.log_stream = kwargs['log_stream']
del kwargs['log_stream']
super(BaseArgParser, self).__init__(*args, **kwargs)
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(),
help=_('Print version information'))
self.group.add_argument('-v', action='version', version='%(prog)s ' + get_version(),
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'))
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.
@ -204,7 +246,7 @@ class BaseArgParser(argparse.ArgumentParser):
argparse.Namespace: The parsed arguments.
"""
options = super(BaseArgParser, self).parse_args(args=args)
options = super(ArgParserBase, self).parse_args(args=args)
return self._handle_ui_options(options)
def parse_known_ui_args(self, args, withhold=None):
@ -220,7 +262,7 @@ class BaseArgParser(argparse.ArgumentParser):
"""
if withhold:
args = [a for a in args if a not in withhold]
options, remaining = super(BaseArgParser, self).parse_known_args(args=args)
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)
@ -251,8 +293,13 @@ class BaseArgParser(argparse.ArgumentParser):
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)
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):
@ -285,11 +332,13 @@ class BaseArgParser(argparse.ArgumentParser):
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)
@ -300,14 +349,39 @@ class BaseArgParser(argparse.ArgumentParser):
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'))
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)'))
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

@ -11,6 +11,8 @@
# 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
@ -31,9 +33,9 @@ def decode_int(x, f):
f += 1
newf = x.index(END_DELIM, f)
n = int(x[f:newf])
if x[f:f+1] == b'-' and x[f+1:f+2] == b'0':
if x[f : f + 1] == b'-' and x[f + 1 : f + 2] == b'0':
raise ValueError
elif x[f:f+1] == b'0' and newf != f + 1:
elif x[f : f + 1] == b'0' and newf != f + 1:
raise ValueError
return (n, newf + 1)
@ -41,25 +43,25 @@ def decode_int(x, f):
def decode_string(x, f):
colon = x.index(BYTE_SEP, f)
n = int(x[f:colon])
if x[f:f+1] == b'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:f+1] != END_DELIM:
v, f = decode_func[x[f:f+1]](x, f)
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:f+1] != END_DELIM:
while x[f : f + 1] != END_DELIM:
k, f = decode_string(x, f)
r[k], f = decode_func[x[f:f+1]](x, f)
r[k], f = decode_func[x[f : f + 1]](x, f)
return (r, f + 1)
@ -81,8 +83,8 @@ decode_func[b'9'] = decode_string
def bdecode(x):
try:
r, l = decode_func[x[0:1]](x, 0)
except (IndexError, KeyError, ValueError):
r, __ = decode_func[x[0:1]](x, 0)
except (LookupError, TypeError, ValueError):
raise BTFailure('Not a valid bencoded string')
else:
return r
@ -109,7 +111,7 @@ def encode_bool(x, r):
def encode_string(x, r):
encode_string(x.encode('utf8'), r)
encode_bytes(x.encode('utf8'), r)
def encode_bytes(x, r):
@ -126,6 +128,10 @@ def encode_list(x, r):
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(END_DELIM)
@ -141,9 +147,9 @@ encode_func[bool] = encode_bool
encode_func[str] = encode_string
encode_func[bytes] = encode_bytes
if PY2:
encode_func[long] = encode_int
encode_func[long] = encode_int # noqa: F821
encode_func[str] = encode_bytes
encode_func[unicode] = encode_string
encode_func[unicode] = encode_string # noqa: F821
def bencode(x):

View File

@ -11,7 +11,7 @@
from __future__ import division, print_function, unicode_literals
import base64
import datetime
import binascii
import functools
import glob
import locale
@ -24,12 +24,20 @@ import subprocess
import sys
import tarfile
import time
from contextlib import closing
from datetime import datetime
from io import BytesIO, open
import chardet
import pkg_resources
from deluge.decorators import deprecated
from deluge.error import InvalidPathError
try:
import chardet
except ImportError:
chardet = None
try:
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
@ -38,19 +46,24 @@ except ImportError:
from urlparse import urljoin # pylint: disable=ungrouped-imports
from urllib import pathname2url, unquote_plus # pylint: disable=ungrouped-imports
DBUS_FILEMAN = None
# gi makes dbus available on Window but don't import it as unused.
# Windows workaround for HTTPS requests requiring certificate authority bundle.
# see: https://twistedmatrix.com/trac/ticket/9209
if platform.system() in ('Windows', 'Microsoft'):
from certifi import where
os.environ['SSL_CERT_FILE'] = where()
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
# gi makes dbus available on Window but don't import it as unused.
try:
import dbus
except ImportError:
pass
else:
try:
bus = dbus.SessionBus()
DBUS_FILEMAN = bus.get_object('org.freedesktop.FileManager1', '/org/freedesktop/FileManager1')
except dbus.DBusException:
pass
dbus = None
try:
import distro
except ImportError:
distro = None
log = logging.getLogger(__name__)
@ -62,7 +75,7 @@ TORRENT_STATE = [
'Paused',
'Error',
'Queued',
'Moving'
'Moving',
]
# The output formatting for json.dump
@ -72,14 +85,12 @@ PY2 = sys.version_info.major == 2
def get_version():
"""
Returns the program version from the egg metadata
:returns: the version of Deluge
:rtype: string
"""The program version from the egg metadata.
Returns:
str: The version of Deluge.
"""
return pkg_resources.require('Deluge')[0].version
return pkg_resources.get_distribution('Deluge').version
def get_default_config_dir(filename=None):
@ -93,6 +104,7 @@ def get_default_config_dir(filename=None):
"""
if windows_check():
def save_config_path(resource):
app_data_path = os.environ.get('APPDATA')
if not app_data_path:
@ -102,11 +114,13 @@ def get_default_config_dir(filename=None):
import _winreg as winreg # For Python 2.
hkey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders')
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
)
app_data_reg = winreg.QueryValueEx(hkey, 'AppData')
app_data_path = app_data_reg[0]
winreg.CloseKey(hkey)
return os.path.join(app_data_path, resource)
else:
from xdg.BaseDirectory import save_config_path
if not filename:
@ -127,11 +141,15 @@ def get_default_download_dir():
download_dir = ''
if not windows_check():
from xdg.BaseDirectory import xdg_config_home
try:
with open(os.path.join(xdg_config_home, 'user-dirs.dirs'), 'r') as _file:
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
with open(user_dirs_path, 'r', encoding='utf8') as _file:
for line in _file:
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
download_dir = os.path.expandvars(line.partition('=')[2].rstrip().strip('"'))
download_dir = os.path.expandvars(
line.partition('=')[2].rstrip().strip('"')
)
break
except IOError:
pass
@ -141,47 +159,58 @@ def get_default_download_dir():
return download_dir
def archive_files(arc_name, filepaths):
"""Compress a list of filepaths into timestamped tarball in config dir.
def archive_files(arc_name, filepaths, message=None, rotate=10):
"""Compress a list of filepaths into timestamped tarball in config dir.
The archiving config directory is 'archive'.
The archiving config directory is 'archive'.
Args:
arc_name (str): The archive output filename (appended with timestamp).
filepaths (list): A list of the files to be archived into tarball.
Args:
arc_name (str): The archive output filename (appended with timestamp).
filepaths (list): A list of the files to be archived into tarball.
Returns:
str: The full archive filepath.
Returns:
str: The full archive filepath.
"""
"""
from deluge.configmanager import get_config_dir
from deluge.configmanager import get_config_dir
# Set archive compression to lzma with bz2 fallback.
arc_comp = 'xz' if not PY2 else 'bz2'
# Set archive compression to lzma with bz2 fallback.
arc_comp = 'xz' if not PY2 else 'bz2'
archive_dir = os.path.join(get_config_dir(), 'archive')
timestamp = datetime.datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
arc_filepath = os.path.join(archive_dir, arc_name + '-' + timestamp + '.tar.' + arc_comp)
max_num_arcs = 20
archive_dir = os.path.join(get_config_dir(), 'archive')
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
arc_filepath = os.path.join(
archive_dir, arc_name + '-' + timestamp + '.tar.' + arc_comp
)
if not os.path.exists(archive_dir):
os.makedirs(archive_dir)
else:
old_arcs = glob.glob(os.path.join(archive_dir, arc_name) + '*')
if len(old_arcs) > max_num_arcs:
# TODO: Remove oldest timestamped archives.
log.warning('More than %s tarballs in config archive', max_num_arcs)
if not os.path.exists(archive_dir):
os.makedirs(archive_dir)
else:
all_arcs = glob.glob(os.path.join(archive_dir, arc_name) + '*')
if len(all_arcs) >= rotate:
log.warning(
'Too many existing archives for %s. Deleting oldest archive.', arc_name
)
os.remove(sorted(all_arcs)[0])
try:
with tarfile.open(arc_filepath, 'w:' + arc_comp) as tf:
for filepath in filepaths:
tf.add(filepath, arcname=os.path.basename(filepath))
except OSError:
log.error('Problem occurred archiving filepaths: %s', filepaths)
return False
else:
return arc_filepath
try:
with tarfile.open(arc_filepath, 'w:' + arc_comp) as tar:
for filepath in filepaths:
if not os.path.isfile(filepath):
continue
tar.add(filepath, arcname=os.path.basename(filepath))
if message:
with closing(BytesIO(message.encode('utf8'))) as fobj:
tarinfo = tarfile.TarInfo('archive_message.txt')
tarinfo.size = len(fobj.getvalue())
tarinfo.mtime = time.time()
tar.addfile(tarinfo, fileobj=fobj)
except OSError:
log.error('Problem occurred archiving filepaths: %s', filepaths)
return False
else:
return arc_filepath
def windows_check():
@ -229,14 +258,25 @@ def linux_check():
def get_os_version():
"""Parse and return the os version information.
Converts the platform ver tuple to a string.
Returns:
str: The os version info.
"""
if windows_check():
return platform.win32_ver()
os_version = platform.win32_ver()
elif osx_check():
return platform.mac_ver()
elif linux_check():
return platform.linux_distribution()
os_version = list(platform.mac_ver())
os_version[1] = '' # versioninfo always empty.
elif distro:
os_version = distro.linux_distribution()
else:
return (platform.release(), )
os_version = (platform.release(),)
return ' '.join(filter(None, os_version))
def get_pixmap(fname):
@ -253,14 +293,17 @@ def get_pixmap(fname):
def resource_filename(module, path):
"""While developing, if there's a second deluge package, installed globally
and another in develop mode somewhere else, while pkg_resources.require('Deluge')
returns the proper deluge instance, pkg_resources.resource_filename does
not, it returns the first found on the python path, which is not good
enough.
This is a work-around that.
"""Get filesystem path for a resource.
This function contains a work-around for pkg_resources.resource_filename
not returning the correct path with multiple packages installed.
So if there's a second deluge package, installed globally and another in
develop mode somewhere else, while pkg_resources.get_distribution('Deluge')
returns the proper deluge instance, pkg_resources.resource_filename
does not, it returns the first found on the python path, which is wrong.
"""
return pkg_resources.require('Deluge>=%s' % get_version())[0].get_resource_filename(
return pkg_resources.get_distribution('Deluge').get_resource_filename(
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
)
@ -281,8 +324,12 @@ def open_file(path, timestamp=None):
if timestamp is None:
timestamp = int(time.time())
env = os.environ.copy()
env['DESKTOP_STARTUP_ID'] = '%s-%u-%s-xdg_open_TIME%d' % \
(os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
env['DESKTOP_STARTUP_ID'] = '%s-%u-%s-xdg_open_TIME%d' % (
os.path.basename(sys.argv[0]),
os.getpid(),
os.uname()[1],
timestamp,
)
subprocess.Popen(['xdg-open', '%s' % path], env=env)
@ -301,10 +348,22 @@ def show_file(path, timestamp=None):
else:
if timestamp is None:
timestamp = int(time.time())
startup_id = '%s_%u_%s-dbus_TIME%d' % (os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
if DBUS_FILEMAN:
startup_id = '%s_%u_%s-dbus_TIME%d TIMESTAMP=%d' % (
os.path.basename(sys.argv[0]),
os.getpid(),
os.uname()[1],
timestamp,
timestamp,
)
if dbus:
bus = dbus.SessionBus()
filemanager1 = bus.get_object(
'org.freedesktop.FileManager1', '/org/freedesktop/FileManager1'
)
paths = [urljoin('file:', pathname2url(path))]
DBUS_FILEMAN.ShowItems(paths, startup_id, dbus_interface='org.freedesktop.FileManager1')
filemanager1.ShowItems(
paths, startup_id, dbus_interface='org.freedesktop.FileManager1'
)
else:
env = os.environ.copy()
env['DESKTOP_STARTUP_ID'] = startup_id.replace('dbus', 'xdg-open')
@ -321,6 +380,7 @@ def open_url_in_browser(url):
"""
import webbrowser
webbrowser.open(url)
@ -376,13 +436,29 @@ def fsize(fsize_b, precision=1, shortform=False):
"""
if fsize_b >= 1024 ** 4:
return '%.*f %s' % (precision, fsize_b / 1024 ** 4, tib_txt_short if shortform else tib_txt)
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 4,
tib_txt_short if shortform else tib_txt,
)
elif fsize_b >= 1024 ** 3:
return '%.*f %s' % (precision, fsize_b / 1024 ** 3, gib_txt_short if shortform else gib_txt)
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 3,
gib_txt_short if shortform else gib_txt,
)
elif fsize_b >= 1024 ** 2:
return '%.*f %s' % (precision, fsize_b / 1024 ** 2, mib_txt_short if shortform else mib_txt)
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 2,
mib_txt_short if shortform else mib_txt,
)
elif fsize_b >= 1024:
return '%.*f %s' % (precision, fsize_b / 1024, kib_txt_short if shortform else kib_txt)
return '%.*f %s' % (
precision,
fsize_b / 1024,
kib_txt_short if shortform else kib_txt,
)
else:
return '%d %s' % (fsize_b, byte_txt)
@ -405,7 +481,7 @@ def fpcnt(dec, precision=2):
"""
pcnt = (dec * 100)
pcnt = dec * 100
if pcnt == 0 or pcnt == 100:
precision = 0
return '%.*f%%' % (precision, pcnt)
@ -427,13 +503,29 @@ def fspeed(bps, precision=1, shortform=False):
"""
if bps < 1024 ** 2:
return '%.*f %s' % (precision, bps / 1024, _('K/s') if shortform else _('KiB/s'))
return '%.*f %s' % (
precision,
bps / 1024,
_('K/s') if shortform else _('KiB/s'),
)
elif bps < 1024 ** 3:
return '%.*f %s' % (precision, bps / 1024 ** 2, _('M/s') if shortform else _('MiB/s'))
return '%.*f %s' % (
precision,
bps / 1024 ** 2,
_('M/s') if shortform else _('MiB/s'),
)
elif bps < 1024 ** 4:
return '%.*f %s' % (precision, bps / 1024 ** 3, _('G/s') if shortform else _('GiB/s'))
return '%.*f %s' % (
precision,
bps / 1024 ** 3,
_('G/s') if shortform else _('GiB/s'),
)
else:
return '%.*f %s' % (precision, bps / 1024 ** 4, _('T/s') if shortform else _('TiB/s'))
return '%.*f %s' % (
precision,
bps / 1024 ** 4,
_('T/s') if shortform else _('TiB/s'),
)
def fpeer(num_peers, total_peers):
@ -463,7 +555,7 @@ def ftime(secs):
"""Formats a string to show time in a human readable form.
Args:
secs (int): The number of seconds.
secs (int or float): The number of seconds.
Returns:
str: A formatted time string or empty string if value is 0.
@ -477,20 +569,22 @@ def ftime(secs):
"""
# Handle floats by truncating to an int
secs = int(secs)
if secs <= 0:
time_str = ''
elif secs < 60:
time_str = '{:d}s'.format(secs)
time_str = '{}s'.format(secs)
elif secs < 3600:
time_str = '{:d}m {:d}s'.format(secs // 60, secs % 60)
time_str = '{}m {}s'.format(secs // 60, secs % 60)
elif secs < 86400:
time_str = '{:d}h {:d}m'.format(secs // 3600, secs // 60 % 60)
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60)
elif secs < 604800:
time_str = '{:d}d {:d}h'.format(secs // 86400, secs // 3600 % 24)
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24)
elif secs < 31449600:
time_str = '{:d}w {:d}d'.format(secs // 604800, secs // 86400 % 7)
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7)
else:
time_str = '{:d}y {:d}w'.format(secs // 31449600, secs // 604800 % 52)
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52)
return time_str
@ -542,18 +636,20 @@ def tokenize(text):
return tokenized_input
size_units = (dict(prefix='b', divider=1, singular='byte', plural='bytes'),
dict(prefix='KiB', divider=1024**1),
dict(prefix='MiB', divider=1024**2),
dict(prefix='GiB', divider=1024**3),
dict(prefix='TiB', divider=1024**4),
dict(prefix='PiB', divider=1024**5),
dict(prefix='KB', divider=1000**1),
dict(prefix='MB', divider=1000**2),
dict(prefix='GB', divider=1000**3),
dict(prefix='TB', divider=1000**4),
dict(prefix='PB', divider=1000**5),
dict(prefix='m', divider=1000**2))
size_units = [
{'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'},
{'prefix': 'KiB', 'divider': 1024 ** 1},
{'prefix': 'MiB', 'divider': 1024 ** 2},
{'prefix': 'GiB', 'divider': 1024 ** 3},
{'prefix': 'TiB', 'divider': 1024 ** 4},
{'prefix': 'PiB', 'divider': 1024 ** 5},
{'prefix': 'KB', 'divider': 1000 ** 1},
{'prefix': 'MB', 'divider': 1000 ** 2},
{'prefix': 'GB', 'divider': 1000 ** 3},
{'prefix': 'TB', 'divider': 1000 ** 4},
{'prefix': 'PB', 'divider': 1000 ** 5},
{'prefix': 'm', 'divider': 1000 ** 2},
]
class InvalidSize(Exception):
@ -648,10 +744,10 @@ def is_magnet(uri):
True
"""
if not uri:
return False
if uri.startswith(MAGNET_SCHEME) and XT_BTIH_PARAM in uri:
return True
return False
return uri.startswith(MAGNET_SCHEME) and XT_BTIH_PARAM in uri
def get_magnet_info(uri):
@ -674,7 +770,7 @@ def get_magnet_info(uri):
"""
tr0_param = 'tr.'
tr0_param_regex = re.compile('^tr.(\d+)=(\S+)')
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
if not uri.startswith(MAGNET_SCHEME):
return {}
@ -682,23 +778,24 @@ def get_magnet_info(uri):
info_hash = None
trackers = {}
tier = 0
for param in uri[len(MAGNET_SCHEME):].split('&'):
for param in uri[len(MAGNET_SCHEME) :].split('&'):
if param.startswith(XT_BTIH_PARAM):
xt_hash = param[len(XT_BTIH_PARAM):]
xt_hash = param[len(XT_BTIH_PARAM) :]
if len(xt_hash) == 32:
try:
info_hash = base64.b32decode(xt_hash.upper()).encode('hex')
infohash_str = base64.b32decode(xt_hash.upper())
except TypeError as ex:
log.debug('Invalid base32 magnet hash: %s, %s', xt_hash, ex)
break
info_hash = binascii.hexlify(infohash_str).decode()
elif is_infohash(xt_hash):
info_hash = xt_hash.lower()
else:
break
elif param.startswith(DN_PARAM):
name = unquote_plus(param[len(DN_PARAM):])
name = unquote_plus(param[len(DN_PARAM) :])
elif param.startswith(TR_PARAM):
tracker = unquote_plus(param[len(TR_PARAM):])
tracker = unquote_plus(param[len(TR_PARAM) :])
trackers[tracker] = tier
tier += 1
elif param.startswith(tr0_param):
@ -711,7 +808,12 @@ def get_magnet_info(uri):
if info_hash:
if not name:
name = info_hash
return {'name': name, 'info_hash': info_hash, 'files_tree': '', 'trackers': trackers}
return {
'name': name,
'info_hash': info_hash,
'files_tree': '',
'trackers': trackers,
}
else:
return {}
@ -729,11 +831,11 @@ def create_magnet_uri(infohash, name=None, trackers=None):
"""
try:
infohash = infohash.decode('hex')
except AttributeError:
pass
infohash = binascii.unhexlify(infohash)
except TypeError:
infohash.encode('utf-8')
uri = [MAGNET_SCHEME, XT_BTIH_PARAM, base64.b32encode(infohash)]
uri = [MAGNET_SCHEME, XT_BTIH_PARAM, base64.b32encode(infohash).decode('utf-8')]
if name:
uri.extend(['&', DN_PARAM, name])
if trackers:
@ -788,6 +890,7 @@ def free_space(path):
if windows_check():
from win32file import GetDiskFreeSpaceEx
return GetDiskFreeSpaceEx(path)[0]
else:
disk_data = os.statvfs(path.encode('utf8'))
@ -831,6 +934,7 @@ def is_ipv4(ip):
"""
import socket
try:
if windows_check():
return socket.inet_aton(ip)
@ -859,6 +963,7 @@ def is_ipv6(ip):
import ipaddress
except ImportError:
import socket
try:
return socket.inet_pton(socket.AF_INET6, ip)
except (socket.error, AttributeError):
@ -894,12 +999,12 @@ def decode_bytes(byte_str, encoding='utf8'):
elif not isinstance(byte_str, bytes):
return byte_str
encodings = [lambda: ('utf8', 'strict'),
lambda: ('iso-8859-1', 'strict'),
lambda: (chardet.detect(byte_str)['encoding'], 'strict'),
lambda: (encoding, 'ignore')]
encodings = [lambda: ('utf8', 'strict'), lambda: ('iso-8859-1', 'strict')]
if chardet:
encodings.append(lambda: (chardet.detect(byte_str)['encoding'], 'strict'))
encodings.append(lambda: (encoding, 'ignore'))
if encoding is not 'utf8':
if encoding.lower() not in ['utf8', 'utf-8']:
encodings.insert(0, lambda: (encoding, 'strict'))
for l in encodings:
@ -910,6 +1015,18 @@ def decode_bytes(byte_str, encoding='utf8'):
return ''
@deprecated
def decode_string(byte_str, encoding='utf8'):
"""Deprecated: Use decode_bytes"""
return decode_bytes(byte_str, encoding)
@deprecated
def utf8_encoded(str_, encoding='utf8'):
"""Deprecated: Use encode or decode_bytes if needed"""
return decode_bytes(str_, encoding).encode('utf8')
def utf8_encode_structure(data):
"""Recursively convert all unicode keys and values in a data structure to utf8.
@ -925,7 +1042,9 @@ def utf8_encode_structure(data):
if isinstance(data, (list, tuple)):
return type(data)([utf8_encode_structure(d) for d in data])
elif isinstance(data, dict):
return dict([utf8_encode_structure(d) for d in data.items()])
return {
utf8_encode_structure(k): utf8_encode_structure(v) for k, v in data.items()
}
elif not isinstance(data, bytes):
try:
return data.encode('utf8')
@ -943,8 +1062,10 @@ class VersionSplit(object):
:type ver: string
"""
def __init__(self, ver):
version_re = re.compile(r"""
version_re = re.compile(
r"""
^
(?P<version>\d+\.\d+) # minimum 'N.N'
(?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
@ -954,7 +1075,9 @@ class VersionSplit(object):
(?P<prerelversion>\d+(?:\.\d+)*)
)?
(?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
$""", re.VERBOSE)
$""",
re.VERBOSE,
)
# Check for PEP 386 compliant version
match = re.search(version_re, ver)
@ -968,25 +1091,27 @@ class VersionSplit(object):
self.version = [int(x) for x in vs[0].split('.') if x.isdigit()]
self.version_string = ''.join(str(x) for x in vs[0].split('.') if x.isdigit())
self.suffix = None
self.dev = False
self.dev = None
if len(vs) > 1:
if vs[1].startswith(('rc', 'a', 'b', 'c')):
self.suffix = vs[1]
if vs[-1].startswith('dev'):
self.dev = vs[-1]
try:
# Store only the dev numeral.
self.dev = int(vs[-1].rsplit('dev')[1])
except ValueError:
# Implicit dev numeral is 0.
self.dev = 0
def get_comparable_versions(self, other):
"""
Returns a 2-tuple of lists for use in the comparison
methods.
"""
# PEP 386 versions with .devN precede release version
if bool(self.dev) != bool(other.dev):
if self.dev != 'dev':
self.dev = not self.dev
if other.dev != 'dev':
other.dev = not other.dev
# PEP 386 versions with .devN precede release version so default
# non-dev versions to infinity while dev versions are ints.
self.dev = float('inf') if self.dev is None else self.dev
other.dev = float('inf') if other.dev is None else other.dev
# If there is no suffix we use z because we want final
# to appear after alpha, beta, and rc alphabetically.
v1 = [self.version, self.suffix or 'z', self.dev]
@ -1018,7 +1143,7 @@ def create_auth_file():
auth_file = deluge.configmanager.get_config_dir('auth')
# Check for auth file and create if necessary
if not os.path.exists(auth_file):
with open(auth_file, 'w') as _file:
with open(auth_file, 'w', encoding='utf8') as _file:
_file.flush()
os.fsync(_file.fileno())
# Change the permissions on the file so only this user can read/write it
@ -1034,48 +1159,54 @@ def create_localclient_account(append=False):
if not os.path.exists(auth_file):
create_auth_file()
with open(auth_file, 'a' if append else 'w') as _file:
_file.write(':'.join([
'localclient',
sha(str(random.random()).encode('utf8')).hexdigest(),
str(AUTH_LEVEL_ADMIN)
]) + '\n')
with open(auth_file, 'a' if append else 'w', encoding='utf8') as _file:
_file.write(
':'.join(
[
'localclient',
sha(str(random.random()).encode('utf8')).hexdigest(),
str(AUTH_LEVEL_ADMIN),
]
)
+ '\n'
)
_file.flush()
os.fsync(_file.fileno())
def get_localhost_auth():
"""Grabs the localclient auth line from the 'auth' file and creates a localhost uri.
"""Grabs the localclient auth line from the 'auth' file and creates a localhost uri.
Returns:
tuple: With the username and password to login as.
Returns:
tuple: With the username and password to login as.
"""
from deluge.configmanager import get_config_dir
"""
from deluge.configmanager import get_config_dir
auth_file = get_config_dir('auth')
if not os.path.exists(auth_file):
from deluge.common import create_localclient_account
create_localclient_account()
auth_file = get_config_dir('auth')
if not os.path.exists(auth_file):
from deluge.common import create_localclient_account
with open(auth_file) as auth:
for line in auth:
line = line.strip()
if line.startswith('#') or not line:
# This is a comment or blank line
continue
create_localclient_account()
lsplit = line.split(':')
with open(auth_file, encoding='utf8') as auth:
for line in auth:
line = line.strip()
if line.startswith('#') or not line:
# This is a comment or blank line
continue
if len(lsplit) == 2:
username, password = lsplit
elif len(lsplit) == 3:
username, password, level = lsplit
else:
log.error('Your auth file is malformed: Incorrect number of fields!')
continue
lsplit = line.split(':')
if username == 'localclient':
return (username, password)
if len(lsplit) == 2:
username, password = lsplit
elif len(lsplit) == 3:
username, password, level = lsplit
else:
log.error('Your auth file is malformed: Incorrect number of fields!')
continue
if username == 'localclient':
return (username, password)
def set_env_variable(name, value):
@ -1109,39 +1240,26 @@ def set_env_variable(name, value):
if windows_check():
from ctypes import windll
from ctypes import cdll
from ctypes.util import find_msvcrt
# Update the copy maintained by Windows (so SysInternals Process Explorer sees it)
try:
result = windll.kernel32.SetEnvironmentVariableW(name, value)
if result == 0:
raise Warning
except Exception:
log.warning('Failed to set Env Var \'%s\' (\'kernel32.SetEnvironmentVariableW\')', name)
result = windll.kernel32.SetEnvironmentVariableW(name, value)
if result == 0:
log.info(
"Failed to set Env Var '%s' (kernel32.SetEnvironmentVariableW)", name
)
else:
log.debug('Set Env Var \'%s\' to \'%s\' (\'kernel32.SetEnvironmentVariableW\')', name, value)
log.debug(
"Set Env Var '%s' to '%s' (kernel32.SetEnvironmentVariableW)",
name,
value,
)
# Update the copy maintained by msvcrt (used by gtk+ runtime)
try:
result = cdll.msvcrt._putenv('%s=%s' % (name, value))
if result != 0:
raise Warning
except Exception:
log.warning('Failed to set Env Var \'%s\' (\'msvcrt._putenv\')', name)
result = cdll.msvcrt._wputenv('%s=%s' % (name, value))
if result != 0:
log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name)
else:
log.debug('Set Env Var \'%s\' to \'%s\' (\'msvcrt._putenv\')', name, value)
# Update the copy maintained by whatever c runtime is used by Python
try:
msvcrt = find_msvcrt()
msvcrtname = str(msvcrt).split('.')[0] if '.' in msvcrt else str(msvcrt)
result = cdll.LoadLibrary(msvcrt)._putenv('%s=%s' % (name, value))
if result != 0:
raise Warning
except Exception:
log.warning('Failed to set Env Var \'%s\' (\'%s._putenv\')', name, msvcrtname)
else:
log.debug('Set Env Var \'%s\' to \'%s\' (\'%s._putenv\')', name, value, msvcrtname)
log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value)
def unicode_argv():
@ -1167,8 +1285,7 @@ def unicode_argv():
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
range(start, argc.value)]
return [argv[i] for i in range(start, argc.value)]
else:
# On other platforms, we have to find the likely encoding of the args and decode
# First check if sys.stdout or stdin have encoding set
@ -1201,6 +1318,7 @@ def run_profiled(func, *args, **kwargs):
"""
if kwargs.get('do_profile', True) is not False:
import cProfile
profiler = cProfile.Profile()
def on_shutdown():
@ -1212,6 +1330,7 @@ def run_profiled(func, *args, **kwargs):
else:
import pstats
from io import StringIO
strio = StringIO()
ps = pstats.Stats(profiler, stream=strio).sort_stats('cumulative')
ps.print_stats()
@ -1239,6 +1358,7 @@ def is_process_running(pid):
if windows_check():
from win32process import EnumProcesses
return pid in EnumProcesses()
else:
try:

View File

@ -13,12 +13,11 @@ import logging
import traceback
from collections import defaultdict
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
from deluge.common import PY2
log = logging.getLogger(__name__)
@ -27,7 +26,6 @@ class ComponentAlreadyRegistered(Exception):
class ComponentException(Exception):
def __init__(self, message, tb):
super(ComponentException, self).__init__(message)
self.message = message
@ -93,6 +91,7 @@ class Component(object):
still be considered in a Started state.
"""
def __init__(self, name, interval=1, depend=None):
"""Initialize component.
@ -146,10 +145,14 @@ class Component(object):
elif self._component_state == 'Started':
d = succeed(True)
else:
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)))
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):
@ -193,10 +196,14 @@ class Component(object):
elif self._component_state == 'Paused':
d = succeed(None)
else:
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)))
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):
@ -207,10 +214,14 @@ class Component(object):
d = maybeDeferred(self._component_start_timer)
d.addCallback(on_resume)
else:
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)))
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):
@ -244,6 +255,7 @@ class ComponentRegistry(object):
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
@ -264,7 +276,9 @@ class ComponentRegistry(object):
"""
name = obj._component_name
if name in self.components:
raise ComponentAlreadyRegistered('Component already registered with name %s' % name)
raise ComponentAlreadyRegistered(
'Component already registered with name %s' % name
)
self.components[obj._component_name] = obj
if obj._component_depend:
@ -289,6 +303,7 @@ class ComponentRegistry(object):
def on_stop(result, 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)
@ -309,7 +324,7 @@ class ComponentRegistry(object):
# Start all the components if names is empty
if not names:
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
elif isinstance(names, string_types):
names = [names]
def on_depends_started(result, name):
@ -343,7 +358,7 @@ class ComponentRegistry(object):
"""
if not names:
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
elif isinstance(names, string_types):
names = [names]
def on_dependents_stopped(result, name):
@ -358,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:
@ -381,7 +398,7 @@ class ComponentRegistry(object):
"""
if not names:
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
elif isinstance(names, string_types):
names = [names]
deferreds = []
@ -407,7 +424,7 @@ class ComponentRegistry(object):
"""
if not names:
names = list(self.components)
elif isinstance(names, str if not PY2 else basestring):
elif isinstance(names, string_types):
names = [names]
deferreds = []
@ -428,8 +445,11 @@ class ComponentRegistry(object):
Deferred: Fired once all Components have been successfully shut down.
"""
def on_stopped(result):
return DeferredList([comp._component_shutdown() for comp in self.components.values()])
return DeferredList(
[comp._component_shutdown() for comp in self.components.values()]
)
return self.stop(list(self.components)).addCallback(on_stopped)

View File

@ -41,18 +41,20 @@ version as this will be done internally.
"""
from __future__ import unicode_literals
import cPickle as pickle
import json
import logging
import os
import shutil
from codecs import getwriter
from io import open
from tempfile import NamedTemporaryFile
import six.moves.cPickle as pickle # noqa: N813
from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__)
callLater = None # Necessary for the config tests
callLater = None # noqa: N816 Necessary for the config tests
def prop(func):
@ -91,8 +93,13 @@ def find_json_objects(s):
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 == '}':
opens -= 1
@ -115,16 +122,14 @@ class Config(object):
setup to convert old config files. (default: 1)
"""
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': file_version
}
self.__version = {'format': 1, 'file': file_version}
# This will get set with a reactor.callLater whenever a config option
# is set.
@ -182,18 +187,21 @@ class Config(object):
if self.__config[key] == value:
return
# Do not allow the type to change unless it is None
if value is not None and not isinstance(
self.__config[key], type(None)) and not isinstance(self.__config[key], type(value)):
# 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:
oldtype = type(self.__config[key])
value = oldtype(value)
# 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('Value Type "%s" invalid for key: %s', type(value), key)
raise
if isinstance(value, bytes):
value.decode('utf8')
value = value.decode('utf8')
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
self.__config[key] = value
@ -201,7 +209,9 @@ class Config(object):
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
from twisted.internet.reactor import (
callLater,
) # pylint: disable=redefined-outer-name
# Run the set_function for this key if any
try:
for func in self.__set_functions[key]:
@ -209,9 +219,11 @@ class Config(object):
except KeyError:
pass
try:
def do_change_callbacks(key, value):
for func in self.__change_callbacks:
func(key, value)
callLater(0, do_change_callbacks, key, value)
except Exception:
pass
@ -297,7 +309,9 @@ class Config(object):
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
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():
@ -422,8 +436,13 @@ class Config(object):
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.
@ -457,8 +476,11 @@ class Config(object):
# Save the new config and make sure it's written to disk
try:
log.debug('Saving new config file %s', filename + '.new')
with open(filename + '.new', 'wb') as _file:
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()
@ -467,6 +489,9 @@ class Config(object):
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.bak', filename)
@ -477,8 +502,8 @@ class Config(object):
# 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)
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
@ -505,16 +530,23 @@ class Config(object):
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)
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 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)
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
@ -527,9 +559,11 @@ class Config(object):
@prop
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

@ -94,9 +94,12 @@ class _ConfigManager(object):
log.debug('Getting config: %s', config_file)
# Create the config object if not already created
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)
self.config_files[config_file] = Config(
config_file,
defaults,
config_dir=self.config_directory,
file_version=file_version,
)
return self.config_files[config_file]
@ -106,7 +109,9 @@ _configmanager = _ConfigManager()
def ConfigManager(config, defaults=None, file_version=1): # NOQA: N802
return _configmanager.get_config(config, defaults=defaults, file_version=file_version)
return _configmanager.get_config(
config, defaults=defaults, file_version=file_version
)
def set_config_dir(directory):

View File

@ -18,6 +18,7 @@ This should typically only be used by the Core. Plugins should utilize the
from __future__ import unicode_literals
import logging
import types
from twisted.internet import reactor
@ -30,6 +31,7 @@ log = logging.getLogger(__name__)
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)
@ -39,13 +41,15 @@ class AlertManager(component.Component):
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)
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})
@ -105,7 +109,10 @@ class AlertManager(component.Component):
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)
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:
@ -118,10 +125,20 @@ class AlertManager(component.Component):
for handler in self.handlers[alert_type]:
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
self.delayed_calls.append(reactor.callLater(0, handler, alert))
# Copy alert attributes
alert_copy = types.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)
component.get('Core').apply_session_setting(
'alert_queue_size', self.alert_queue_size
)

View File

@ -17,8 +17,14 @@ from io import open
import deluge.component as component
import deluge.configmanager as configmanager
from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL,
AUTH_LEVEL_READONLY, create_localclient_account)
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__)
@ -28,7 +34,8 @@ 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()}
@ -45,12 +52,14 @@ 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>' %
{'username': self.username, 'authlevel': self.authlevel})
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
'username': self.username,
'authlevel': self.authlevel,
}
class AuthManager(component.Component):
@ -129,8 +138,9 @@ class AuthManager(component.Component):
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 as ex:
@ -181,7 +191,10 @@ class AuthManager(component.Component):
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.write(
'%(username)s:%(password)s:%(authlevel_int)s\n'
% account.data()
)
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
@ -232,8 +245,12 @@ class AuthManager(component.Component):
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:
@ -254,7 +271,10 @@ 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)

View File

@ -10,26 +10,31 @@
from __future__ import division, unicode_literals
import base64
import glob
import logging
import os
import shutil
import tempfile
import threading
from base64 import b64decode, b64encode
from six import string_types
from twisted.internet import defer, reactor, task
from twisted.web.client import getPage
from twisted.web.client import Agent, readBody
import deluge.common
import deluge.component as component
from deluge import path_chooser_common
from deluge._libtorrent import lt
from deluge.common import PY2
from deluge._libtorrent import LT_VERSION, lt
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.alertmanager import AlertManager
from deluge.core.authmanager import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVELS_MAPPING,
AUTH_LEVELS_MAPPING_REVERSE, AuthManager)
from deluge.core.authmanager import (
AUTH_LEVEL_ADMIN,
AUTH_LEVEL_NONE,
AUTH_LEVELS_MAPPING,
AUTH_LEVELS_MAPPING_REVERSE,
AuthManager,
)
from deluge.core.eventmanager import EventManager
from deluge.core.filtermanager import FilterManager
from deluge.core.pluginmanager import PluginManager
@ -37,8 +42,18 @@ from deluge.core.preferencesmanager import PreferencesManager
from deluge.core.rpcserver import export
from deluge.core.torrentmanager import TorrentManager
from deluge.decorators import deprecated
from deluge.error import AddTorrentError, DelugeError, InvalidPathError, InvalidTorrentError
from deluge.event import NewVersionAvailableEvent, SessionPausedEvent, SessionResumedEvent, TorrentQueueChangedEvent
from deluge.error import (
AddTorrentError,
DelugeError,
InvalidPathError,
InvalidTorrentError,
)
from deluge.event import (
NewVersionAvailableEvent,
SessionPausedEvent,
SessionResumedEvent,
TorrentQueueChangedEvent,
)
from deluge.httpdownloader import download_file
try:
@ -49,7 +64,7 @@ except ImportError:
log = logging.getLogger(__name__)
OLD_SESSION_STATUS_KEYS = {
DEPR_SESSION_STATUS_KEYS = {
# 'active_requests': None, # In dht_stats_alert, if required.
'allowed_upload_slots': 'ses.num_unchoke_slots',
# 'dht_global_nodes': None,
@ -81,9 +96,7 @@ OLD_SESSION_STATUS_KEYS = {
# 'utp_stats': None
}
# TODO: replace with dynamic rate e.g.
# 'dht.dht_bytes_in'.replace('_bytes', '') + '_rate'
# would become 'dht.dht_in_rate'
# Session status rate keys associated with session status counters.
SESSION_RATES_MAPPING = {
'dht_download_rate': 'dht.dht_bytes_in',
'dht_upload_rate': 'dht.dht_bytes_out',
@ -97,25 +110,24 @@ SESSION_RATES_MAPPING = {
'upload_rate': 'net.sent_bytes',
}
DELUGE_VER = deluge.common.get_version()
class Core(component.Component):
def __init__(self, listen_interface=None, read_only_config_keys=None):
log.debug('Core init...')
def __init__(
self, listen_interface=None, outgoing_interface=None, read_only_config_keys=None
):
component.Component.__init__(self, 'Core')
deluge_version = deluge.common.get_version()
split_version = deluge.common.VersionSplit(deluge_version).version
while len(split_version) < 4:
split_version.append(0)
deluge_fingerprint = lt.generate_fingerprint('DE', *split_version)
user_agent = 'Deluge/{} libtorrent/{}'.format(deluge_version, self.get_libtorrent_version())
# Start the libtorrent session.
log.debug('Starting session (fingerprint: %s, user_agent: %s)', deluge_fingerprint, user_agent)
settings_pack = {'peer_fingerprint': deluge_fingerprint,
'user_agent': user_agent,
'ignore_resume_timestamps': True}
user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION)
peer_id = self._create_peer_id(DELUGE_VER)
log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent)
settings_pack = {
'peer_fingerprint': peer_id,
'user_agent': user_agent,
'ignore_resume_timestamps': True,
}
self.session = lt.session(settings_pack, flags=0)
# Load the settings, if available.
@ -141,7 +153,9 @@ class Core(component.Component):
# External IP Address from libtorrent
self.external_ip = None
self.eventmanager.register_event_handler('ExternalIPEvent', self._on_external_ip_event)
self.eventmanager.register_event_handler(
'ExternalIPEvent', self._on_external_ip_event
)
# GeoIP instance with db loaded
self.geoip_instance = None
@ -157,23 +171,38 @@ class Core(component.Component):
# If there was an interface value from the command line, use it, but
# store the one in the config so we can restore it on shutdown
self.__old_interface = None
self._old_listen_interface = None
if listen_interface:
if deluge.common.is_ip(listen_interface):
self.__old_interface = self.config['listen_interface']
self._old_listen_interface = self.config['listen_interface']
self.config['listen_interface'] = listen_interface
else:
log.error('Invalid listen interface (must be IP Address): %s', listen_interface)
log.error(
'Invalid listen interface (must be IP Address): %s',
listen_interface,
)
self._old_outgoing_interface = None
if outgoing_interface:
self._old_outgoing_interface = self.config['outgoing_interface']
self.config['outgoing_interface'] = outgoing_interface
# New release check information
self.__new_release = None
# Session status timer
self.session_status = {}
self.session_status = {k.name: 0 for k in lt.session_stats_metrics()}
self._session_prev_bytes = {k: 0 for k in SESSION_RATES_MAPPING}
# Initiate other session status keys.
self.session_status.update(self._session_prev_bytes)
hit_ratio_keys = ['write_hit_ratio', 'read_hit_ratio']
self.session_status.update({k: 0.0 for k in hit_ratio_keys})
self.session_status_timer_interval = 0.5
self.session_status_timer = task.LoopingCall(self.session.post_session_stats)
self.alertmanager.register_handler('session_stats_alert', self._on_alert_session_stats)
self._session_rates = {(k_rate, k_bytes): 0 for k_rate, k_bytes in SESSION_RATES_MAPPING.items()}
self.alertmanager.register_handler(
'session_stats_alert', self._on_alert_session_stats
)
self.session_rates_timer_interval = 2
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
@ -195,8 +224,11 @@ class Core(component.Component):
self._save_session_state()
# We stored a copy of the old interface value
if self.__old_interface:
self.config['listen_interface'] = self.__old_interface
if self._old_listen_interface is not None:
self.config['listen_interface'] = self._old_listen_interface
if self._old_outgoing_interface is not None:
self.config['outgoing_interface'] = self._old_outgoing_interface
# Make sure the config file has been saved
self.config.save()
@ -216,6 +248,48 @@ class Core(component.Component):
"""
self.session.apply_settings(settings)
@staticmethod
def _create_peer_id(version):
"""Create a peer_id fingerprint.
This creates the peer_id and modifies the release char to identify
pre-release and development version. Using ``D`` for dev, daily or
nightly builds, ``a, b, r`` for pre-releases and ``s`` for
stable releases.
Examples:
``--<client><client><major><minor><micro><release>--``
``--DE200D--`` (development version of 2.0.0)
``--DE200s--`` (stable release of v2.0.0)
``--DE201b--`` (beta pre-release of v2.0.1)
Args:
version (str): The version string in PEP440 dotted notation.
Returns:
str: The formattted peer_id with Deluge prefix e.g. '--DE200s--'
"""
split = deluge.common.VersionSplit(version)
# Fill list with zeros to length of 4 and use lt to create fingerprint.
version_list = split.version + [0] * (4 - len(split.version))
peer_id = lt.generate_fingerprint('DE', *version_list)
def substitute_chr(string, idx, char):
"""Fast substitute single char in string."""
return string[:idx] + char + string[idx + 1 :]
if split.dev:
release_chr = 'D'
elif split.suffix:
# a (alpha), b (beta) or r (release candidate).
release_chr = split.suffix[0].lower()
else:
release_chr = 's'
peer_id = substitute_chr(peer_id, 6, release_chr)
return peer_id
def _save_session_state(self):
"""Saves the libtorrent session state"""
filename = 'session.state'
@ -267,58 +341,71 @@ class Core(component.Component):
def _on_alert_session_stats(self, alert):
"""The handler for libtorrent session stats alert"""
if not self.session_status:
# Empty dict on startup so needs populated with session rate keys and default value.
self.session_status.update({key: 0 for key in list(SESSION_RATES_MAPPING)})
self.session_status.update(alert.values)
self._update_session_cache_hit_ratio()
def _update_session_cache_hit_ratio(self):
"""Calculates the cache read/write hit ratios and updates session_status"""
try:
self.session_status['write_hit_ratio'] = ((self.session_status['disk.num_blocks_written'] -
self.session_status['disk.num_write_ops']) /
self.session_status['disk.num_blocks_written'])
except ZeroDivisionError:
"""Calculates the cache read/write hit ratios for session_status."""
blocks_written = self.session_status['disk.num_blocks_written']
blocks_read = self.session_status['disk.num_blocks_read']
if blocks_written:
self.session_status['write_hit_ratio'] = (
blocks_written - self.session_status['disk.num_write_ops']
) / blocks_written
else:
self.session_status['write_hit_ratio'] = 0.0
try:
self.session_status['read_hit_ratio'] = (self.session_status['disk.num_blocks_cache_hits'] /
self.session_status['disk.num_blocks_read'])
except ZeroDivisionError:
if blocks_read:
self.session_status['read_hit_ratio'] = (
self.session_status['disk.num_blocks_cache_hits'] / blocks_read
)
else:
self.session_status['read_hit_ratio'] = 0.0
def _update_session_rates(self):
"""Calculates status rates based on interval and value difference for session_status"""
if not self.session_status:
return
"""Calculate session status rates.
for (rate_key, status_key), prev_bytes in list(self._session_rates.items()):
new_bytes = self.session_status[status_key]
byte_rate = (new_bytes - prev_bytes) / self.session_rates_timer_interval
self.session_status[rate_key] = byte_rate
Uses polling interval and counter difference for session_status rates.
"""
for rate_key, prev_bytes in list(self._session_prev_bytes.items()):
new_bytes = self.session_status[SESSION_RATES_MAPPING[rate_key]]
self.session_status[rate_key] = (
new_bytes - prev_bytes
) / self.session_rates_timer_interval
# Store current value for next update.
self._session_rates[(rate_key, status_key)] = new_bytes
self._session_prev_bytes[rate_key] = new_bytes
def get_new_release(self):
log.debug('get_new_release')
try:
self.new_release = urlopen('http://download.deluge-torrent.org/version-2.0').read().strip()
self.new_release = (
urlopen('http://download.deluge-torrent.org/version-2.0')
.read()
.decode()
.strip()
)
except URLError as ex:
log.debug('Unable to get release info from website: %s', ex)
return
self.check_new_release()
else:
self.check_new_release()
def check_new_release(self):
if self.new_release:
log.debug('new_release: %s', self.new_release)
if deluge.common.VersionSplit(self.new_release) > deluge.common.VersionSplit(deluge.common.get_version()):
component.get('EventManager').emit(NewVersionAvailableEvent(self.new_release))
if deluge.common.VersionSplit(
self.new_release
) > deluge.common.VersionSplit(deluge.common.get_version()):
component.get('EventManager').emit(
NewVersionAvailableEvent(self.new_release)
)
return self.new_release
return False
def _add_torrent_file(self, filename, filedump, options, save_state=True):
"""Adds a torrent file to the session.
# Exported Methods
@export
def add_torrent_file_async(self, filename, filedump, options, save_state=True):
"""Adds a torrent file to the session asynchonously.
Args:
filename (str): The filename of the torrent.
@ -327,17 +414,20 @@ class Core(component.Component):
save_state (bool): If the state should be saved after adding the file.
Returns:
str: The torrent ID or None.
Deferred: The torrent ID or None.
"""
try:
filedump = base64.decodestring(filedump)
except Exception as ex:
filedump = b64decode(filedump)
except TypeError as ex:
log.error('There was an error decoding the filedump string: %s', ex)
try:
d = self.torrentmanager.add(
filedump=filedump, options=options, filename=filename, save_state=save_state
d = self.torrentmanager.add_async(
filedump=filedump,
options=options,
filename=filename,
save_state=save_state,
)
except RuntimeError as ex:
log.error('There was an error adding the torrent file %s: %s', filename, ex)
@ -345,7 +435,32 @@ class Core(component.Component):
else:
return d
# Exported Methods
@export
def prefetch_magnet_metadata(self, magnet, timeout=30):
"""Download magnet metadata without adding to Deluge session.
Used by UIs to get magnet files for selection before adding to session.
Args:
magnet (str): The magnet uri.
timeout (int): Number of seconds to wait before cancelling request.
Returns:
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
"""
def on_metadata(result, result_d):
"""Return result of torrent_id and metadata"""
result_d.callback(result)
return result
d = self.torrentmanager.prefetch_metadata(magnet, timeout)
# Use a seperate callback chain to handle existing prefetching magnet.
result_d = defer.Deferred()
d.addBoth(on_metadata, result_d)
return result_d
@export
def add_torrent_file(self, filename, filedump, options):
"""Adds a torrent file to the session.
@ -357,13 +472,23 @@ class Core(component.Component):
Returns:
str: The torrent_id or None.
"""
return self._add_torrent_file(filename, filedump, options)
try:
filedump = b64decode(filedump)
except Exception as ex:
log.error('There was an error decoding the filedump string: %s', ex)
try:
return self.torrentmanager.add(
filedump=filedump, options=options, filename=filename
)
except RuntimeError as ex:
log.error('There was an error adding the torrent file %s: %s', filename, ex)
raise
@export
def add_torrent_files(self, torrent_files):
"""Adds multiple torrent files to the session.
"""Adds multiple torrent files to the session asynchonously.
Args:
torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options).
@ -372,18 +497,21 @@ class Core(component.Component):
Deferred
"""
@defer.inlineCallbacks
def add_torrents():
errors = []
last_index = len(torrent_files) - 1
for idx, torrent in enumerate(torrent_files):
try:
yield self._add_torrent_file(torrent[0], torrent[1],
torrent[2], save_state=idx == last_index)
yield self.add_torrent_file_async(
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
)
except AddTorrentError as ex:
log.warn('Error when adding torrent: %s', ex)
log.warning('Error when adding torrent: %s', ex)
errors.append(ex)
defer.returnValue(errors)
return task.deferLater(reactor, 0, add_torrents)
@export
@ -411,7 +539,7 @@ class Core(component.Component):
os.remove(filename)
except OSError as ex:
log.warning('Could not remove temp file: %s', ex)
return self.add_torrent_file(filename, base64.encodestring(data), options)
return self.add_torrent_file(filename, b64encode(data), options)
def on_download_fail(failure):
# Log the error and pass the failure onto the client
@ -481,14 +609,19 @@ class Core(component.Component):
errors = []
for torrent_id in torrent_ids:
try:
self.torrentmanager.remove(torrent_id, remove_data=remove_data, save_state=False)
self.torrentmanager.remove(
torrent_id, remove_data=remove_data, save_state=False
)
except InvalidTorrentError as ex:
errors.append((torrent_id, str(ex)))
# Save the session state
self.torrentmanager.save_state()
if errors:
log.warn('Failed to remove %d of %d torrents.', len(errors), len(torrent_ids))
log.warning(
'Failed to remove %d of %d torrents.', len(errors), len(torrent_ids)
)
return errors
return task.deferLater(reactor, 0, do_remove_torrents)
@export
@ -504,24 +637,22 @@ class Core(component.Component):
:rtype: dict
"""
if not self.session_status:
return {key: 0 for key in keys}
if not keys:
return self.session_status
status = {}
for key in keys:
if key in OLD_SESSION_STATUS_KEYS:
new_key = OLD_SESSION_STATUS_KEYS[key]
log.warning('Using deprecated session status key %s, please use %s', key, new_key)
status[key] = self.session_status[new_key]
else:
try:
status[key] = self.session_status[key]
except KeyError:
log.warning('Session status key does not exist: %s', key)
try:
status[key] = self.session_status[key]
except KeyError:
if key in DEPR_SESSION_STATUS_KEYS:
new_key = DEPR_SESSION_STATUS_KEYS[key]
log.debug(
'Deprecated session status key %s, please use %s', key, new_key
)
status[key] = self.session_status[new_key]
else:
log.warning('Session status key not valid: %s', key)
return status
@export
@ -531,11 +662,21 @@ class Core(component.Component):
self.torrentmanager[torrent_id].force_reannounce()
@export
def pause_torrent(self, torrent_ids):
log.debug('Pausing: %s', torrent_ids)
def pause_torrent(self, torrent_id):
"""Pauses a torrent"""
log.debug('Pausing: %s', torrent_id)
if not isinstance(torrent_id, string_types):
self.pause_torrents(torrent_id)
else:
self.torrentmanager[torrent_id].pause()
@export
def pause_torrents(self, torrent_ids=None):
"""Pauses a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
for torrent_id in torrent_ids:
if not self.torrentmanager[torrent_id].pause():
log.warning('Error pausing torrent %s', torrent_id)
self.pause_torrent(torrent_id)
@export
def connect_peer(self, torrent_id, ip, port):
@ -552,14 +693,14 @@ class Core(component.Component):
@export
def pause_session(self):
"""Pause all torrents in the session"""
"""Pause the entire session"""
if not self.session.is_paused():
self.session.pause()
component.get('EventManager').emit(SessionPausedEvent())
@export
def resume_session(self):
"""Resume all torrents in the session"""
"""Resume the entire session"""
if self.session.is_paused():
self.session.resume()
for torrent_id in self.torrentmanager.torrents:
@ -567,16 +708,43 @@ class Core(component.Component):
component.get('EventManager').emit(SessionResumedEvent())
@export
def resume_torrent(self, torrent_ids):
log.debug('Resuming: %s', torrent_ids)
for torrent_id in torrent_ids:
def is_session_paused(self):
"""Returns the activity of the session"""
return self.session.is_paused()
@export
def resume_torrent(self, torrent_id):
"""Resumes a torrent"""
log.debug('Resuming: %s', torrent_id)
if not isinstance(torrent_id, string_types):
self.resume_torrents(torrent_id)
else:
self.torrentmanager[torrent_id].resume()
def create_torrent_status(self, torrent_id, torrent_keys, plugin_keys, diff=False, update=False, all_keys=False):
@export
def resume_torrents(self, torrent_ids=None):
"""Resumes a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
for torrent_id in torrent_ids:
self.resume_torrent(torrent_id)
def create_torrent_status(
self,
torrent_id,
torrent_keys,
plugin_keys,
diff=False,
update=False,
all_keys=False,
):
try:
status = self.torrentmanager[torrent_id].get_status(torrent_keys, diff, update=update, all_keys=all_keys)
status = self.torrentmanager[torrent_id].get_status(
torrent_keys, diff, update=update, all_keys=all_keys
)
except KeyError:
import traceback
traceback.print_exc()
# Torrent was probaly removed meanwhile
return {}
@ -588,9 +756,17 @@ class Core(component.Component):
@export
def get_torrent_status(self, torrent_id, keys, diff=False):
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(keys, [torrent_id])
return self.create_torrent_status(torrent_id, torrent_keys, plugin_keys, diff=diff, update=True,
all_keys=not keys)
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
keys, [torrent_id]
)
return self.create_torrent_status(
torrent_id,
torrent_keys,
plugin_keys,
diff=diff,
update=True,
all_keys=not keys,
)
@export
def get_torrents_status(self, filter_dict, keys, diff=False):
@ -605,8 +781,11 @@ class Core(component.Component):
# Ask the plugin manager to fill in the plugin keys
if len(plugin_keys) > 0:
for key in status_dict:
status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
status_dict[key].update(
self.pluginmanager.get_status(key, plugin_keys)
)
return status_dict
d.addCallback(add_plugin_fields)
return d
@ -637,7 +816,7 @@ class Core(component.Component):
@export
def get_config_values(self, keys):
"""Get the config values for the entered keys"""
return dict((key, self.config.get(key)) for key in keys)
return {key: self.config.get(key) for key in keys}
@export
def set_config(self, config):
@ -668,7 +847,9 @@ class Core(component.Component):
settings = self.session.get_settings()
proxy_type = settings['proxy_type']
proxy_hostname = settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname']
proxy_hostname = (
settings['i2p_hostname'] if proxy_type == 6 else settings['proxy_hostname']
)
proxy_port = settings['i2p_port'] if proxy_type == 6 else settings['proxy_port']
proxy_dict = {
'type': proxy_type,
@ -678,7 +859,7 @@ class Core(component.Component):
'port': proxy_port,
'proxy_hostnames': settings['proxy_hostnames'],
'proxy_peer_connections': settings['proxy_peer_connections'],
'proxy_tracker_connections': settings['proxy_tracker_connections']
'proxy_tracker_connections': settings['proxy_tracker_connections'],
}
return proxy_dict
@ -715,10 +896,10 @@ class Core(component.Component):
torrent_ids (list): A list of torrent_ids to set the options for.
options (dict): A dict of torrent options to set. See torrent.TorrentOptions class for valid keys.
"""
if 'owner' in options and not self.core.authmanager.has_account(options['owner']):
if 'owner' in options and not self.authmanager.has_account(options['owner']):
raise DelugeError('Username "%s" is not known.' % options['owner'])
if isinstance(torrent_ids, str if not PY2 else basestring):
if isinstance(torrent_ids, string_types):
torrent_ids = [torrent_ids]
for torrent_id in torrent_ids:
@ -808,26 +989,52 @@ class Core(component.Component):
return deluge.common.get_path_size(path)
@export
def create_torrent(self, path, tracker, piece_length, comment, target,
webseeds, private, created_by, trackers, add_to_session):
def create_torrent(
self,
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
):
log.debug('creating torrent..')
threading.Thread(target=self._create_torrent_thread,
args=(
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session)).start()
threading.Thread(
target=self._create_torrent_thread,
args=(
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
),
).start()
def _create_torrent_thread(self, path, tracker, piece_length, comment, target,
webseeds, private, created_by, trackers, add_to_session):
def _create_torrent_thread(
self,
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
):
from deluge import metafile
metafile.make_meta_file(
path,
tracker,
@ -837,13 +1044,14 @@ class Core(component.Component):
webseeds=webseeds,
private=private,
created_by=created_by,
trackers=trackers)
trackers=trackers,
)
log.debug('torrent created!')
if add_to_session:
options = {}
options['download_location'] = os.path.split(path)[0]
with open(target, 'rb') as _file:
filedump = base64.encodestring(_file.read())
filedump = b64encode(_file.read())
self.add_torrent_file(os.path.split(target)[1], filedump, options)
@export
@ -854,8 +1062,8 @@ class Core(component.Component):
ie, plugin_file.read()"""
try:
filedump = base64.decodestring(filedump)
except Exception as ex:
filedump = b64decode(filedump)
except TypeError as ex:
log.error('There was an error decoding the filedump string!')
log.exception(ex)
return
@ -920,7 +1128,9 @@ class Core(component.Component):
def queue_top(self, torrent_ids):
log.debug('Attempting to queue %s to top', torrent_ids)
# torrent_ids must be sorted in reverse before moving to preserve order
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True):
for torrent_id in sorted(
torrent_ids, key=self.torrentmanager.get_queue_position, reverse=True
):
try:
# If the queue method returns True, then we should emit a signal
if self.torrentmanager.queue_top(torrent_id):
@ -931,7 +1141,10 @@ class Core(component.Component):
@export
def queue_up(self, torrent_ids):
log.debug('Attempting to queue %s to up', torrent_ids)
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
for torrent_id in torrent_ids
)
torrent_moved = True
prev_queue_position = None
# torrent_ids must be sorted before moving.
@ -941,7 +1154,9 @@ class Core(component.Component):
try:
torrent_moved = self.torrentmanager.queue_up(torrent_id)
except KeyError:
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
log.warning(
'torrent_id: %s does not exist in the queue', torrent_id
)
# If the torrent moved, then we should emit a signal
if torrent_moved:
component.get('EventManager').emit(TorrentQueueChangedEvent())
@ -951,7 +1166,10 @@ class Core(component.Component):
@export
def queue_down(self, torrent_ids):
log.debug('Attempting to queue %s to down', torrent_ids)
torrents = ((self.torrentmanager.get_queue_position(torrent_id), torrent_id) for torrent_id in torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
for torrent_id in torrent_ids
)
torrent_moved = True
prev_queue_position = None
# torrent_ids must be sorted before moving.
@ -961,7 +1179,9 @@ class Core(component.Component):
try:
torrent_moved = self.torrentmanager.queue_down(torrent_id)
except KeyError:
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
log.warning(
'torrent_id: %s does not exist in the queue', torrent_id
)
# If the torrent moved, then we should emit a signal
if torrent_moved:
component.get('EventManager').emit(TorrentQueueChangedEvent())
@ -972,7 +1192,9 @@ class Core(component.Component):
def queue_bottom(self, torrent_ids):
log.debug('Attempting to queue %s to bottom', torrent_ids)
# torrent_ids must be sorted before moving to preserve order
for torrent_id in sorted(torrent_ids, key=self.torrentmanager.get_queue_position):
for torrent_id in sorted(
torrent_ids, key=self.torrentmanager.get_queue_position
):
try:
# If the queue method returns True, then we should emit a signal
if self.torrentmanager.queue_bottom(torrent_id):
@ -993,16 +1215,18 @@ class Core(component.Component):
:rtype: bool
"""
d = getPage(b'http://deluge-torrent.org/test_port.php?port=%s' %
self.get_listen_port(), timeout=30)
port = self.get_listen_port()
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
agent = Agent(reactor, connectTimeout=30)
d = agent.request(b'GET', url.encode())
def on_get_page(result):
return bool(int(result))
def on_get_page(body):
return bool(int(body))
def on_error(failure):
log.warning('Error testing listen port: %s', failure)
d.addCallback(on_get_page)
d.addCallback(readBody).addCallback(on_get_page)
d.addErrback(on_error)
return d
@ -1047,7 +1271,7 @@ class Core(component.Component):
:rtype: string
"""
return lt.__version__
return LT_VERSION
@export
def get_completion_paths(self, args):

View File

@ -65,40 +65,59 @@ def is_daemon_running(pid_file):
class Daemon(object):
"""The Deluge Daemon class"""
def __init__(self, listen_interface=None, interface=None, port=None, standalone=False,
read_only_config_keys=None):
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.
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.
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!')
raise DaemonRunningError(
'Deluge daemon already running with this config directory!'
)
# 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 windows_check():
def win_handler(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)
# Start the core as a thread and join it until it's done
self.core = Core(listen_interface=listen_interface,
read_only_config_keys=read_only_config_keys)
self.core = Core(
listen_interface=listen_interface,
outgoing_interface=outgoing_interface,
read_only_config_keys=read_only_config_keys,
)
if port is None:
port = self.core.config['daemon_port']
@ -112,10 +131,16 @@ class Daemon(object):
port=port,
allow_remote=self.core.config['allow_remote'],
listen=not standalone,
interface=interface
interface=interface,
)
log.debug('Listening to UI on: %s:%s and bittorrent on: %s', interface, port, listen_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
@ -157,6 +182,11 @@ class Daemon(object):
"""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):
"""Determines if session auth_level is authorized to call RPC.
@ -170,4 +200,6 @@ class Daemon(object):
if rpc not in self.get_method_list():
return False
return self.rpcserver.get_session_auth_level() >= self.rpcserver.get_rpc_auth_level(rpc)
return self.rpcserver.get_session_auth_level() >= self.rpcserver.get_rpc_auth_level(
rpc
)

View File

@ -15,22 +15,55 @@ 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.ui.baseargparser import BaseArgParser
from deluge.ui.translations_util import set_dummy_trans
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('--read-only-config-keys', metavar='<comma-separated-keys>', action='store',
help=_('Config keys to be unmodified by `set_config` RPC'), type=str, default='')
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()
@ -45,20 +78,23 @@ def start_daemon(skip_start=False):
deluge.core.daemon.Daemon: A new daemon object
"""
set_dummy_trans(warn_msg=True)
setup_mock_translation()
# Setup the argument parser
parser = BaseArgParser()
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)
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__)
@ -72,18 +108,25 @@ def start_daemon(skip_start=False):
def run_daemon(options):
try:
from deluge.core.daemon import Daemon
daemon = Daemon(listen_interface=options.listen_interface,
interface=options.ui_interface,
port=options.port,
read_only_config_keys=options.read_only_config_keys.split(','))
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)
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)
@ -95,4 +138,6 @@ def start_daemon(skip_start=False):
if options.pidfile:
os.remove(options.pidfile)
return run_profiled(run_daemon, options, output_file=options.profile, do_profile=options.profile)
return run_profiled(
run_daemon, options, output_file=options.profile, do_profile=options.profile
)

View File

@ -36,7 +36,12 @@ class EventManager(component.Component):
try:
handler(*event.args)
except Exception as ex:
log.error('Event handler %s failed in %s with exception %s', event.name, handler, 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

@ -11,8 +11,10 @@ from __future__ import unicode_literals
import logging
from six import string_types
import deluge.component as component
from deluge.common import PY2, TORRENT_STATE
from deluge.common import TORRENT_STATE
log = logging.getLogger(__name__)
@ -101,6 +103,7 @@ class FilterManager(component.Component):
"""FilterManager
"""
def __init__(self, core):
component.Component.__init__(self, 'FilterManager')
log.debug('FilterManager init..')
@ -115,12 +118,14 @@ class FilterManager(component.Component):
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)
def filter_torrent_ids(self, filter_dict):
@ -133,7 +138,7 @@ class FilterManager(component.Component):
# Sanitize input: filter-value must be a list of strings
for key, value in filter_dict.items():
if isinstance(value, str if not PY2 else basestring):
if isinstance(value, string_types):
filter_dict[key] = [value]
# Optimized filter for id
@ -162,19 +167,25 @@ class FilterManager(component.Component):
return torrent_ids
# Registered filters
for field, values in filter_dict.items():
for field, values in list(filter_dict.items()):
if field in self.registered_filters:
# Filters out doubles
torrent_ids = list(set(self.registered_filters[field](torrent_ids, values)))
torrent_ids = list(
set(self.registered_filters[field](torrent_ids, values))
)
del filter_dict[field]
if not filter_dict:
return torrent_ids
torrent_keys, plugin_keys = self.torrents.separate_keys(list(filter_dict), torrent_ids)
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.core.create_torrent_status(torrent_id, torrent_keys, plugin_keys)
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
@ -194,17 +205,21 @@ class FilterManager(component.Component):
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',)))
items['tracker_host']['Error'] = len(
tracker_error_filter(torrent_ids, ('Error',))
)
if not show_zero_hits:
for cat in ['state', 'owner', 'tracker_host']:
@ -215,7 +230,7 @@ class FilterManager(component.Component):
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)
sorted_items['state'].sort(key=self._sort_state_item)
return sorted_items
@ -224,7 +239,9 @@ class FilterManager(component.Component):
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()))
init_state['Active'] = len(
self.filter_state_active(self.torrents.get_torrent_list())
)
return init_state
def register_filter(self, filter_id, filter_func, filter_value=None):
@ -242,7 +259,9 @@ 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'])
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:
@ -251,18 +270,12 @@ class FilterManager(component.Component):
def _hide_state_items(self, state_items):
"""For hide(show)-zero hits"""
for (value, count) in state_items.items():
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

@ -33,7 +33,8 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Compon
# 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
@ -76,6 +77,7 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, component.Compon
if name not in self.plugins:
component.get('EventManager').emit(PluginDisabledEvent(name))
return result
d.addBoth(on_disable_plugin)
return d

View File

@ -47,6 +47,7 @@ DEFAULT_PREFS = {
'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,
@ -71,8 +72,11 @@ DEFAULT_PREFS = {
'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_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,
@ -122,7 +126,7 @@ DEFAULT_PREFS = {
'cache_expiry': 60,
'auto_manage_prefer_seeds': False,
'shared': False,
'super_seeding': False
'super_seeding': False,
}
@ -131,7 +135,9 @@ class PreferencesManager(component.Component):
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')
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']
@ -187,6 +193,11 @@ class PreferencesManager(component.Component):
def _on_set_listen_interface(self, key, value):
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):
self.__set_listen_on()
@ -195,20 +206,34 @@ class PreferencesManager(component.Component):
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
listen_ports = [
self.config['listen_random_port']
] * 2 # use single port range
else:
self.config['listen_random_port'] = None
listen_ports = self.config['listen_ports']
interface = str(self.config['listen_interface'].strip())
interface = interface if interface else '0.0.0.0'
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)]
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)})
{
'listen_system_port_fallback': self.config['listen_use_sys_port'],
'listen_interfaces': ''.join(interfaces),
}
)
def _on_set_outgoing_ports(self, key, value):
self.__set_outgoing_ports()
@ -217,14 +242,22 @@ class PreferencesManager(component.Component):
self.__set_outgoing_ports()
def __set_outgoing_ports(self):
port = 0 if self.config['random_outgoing_ports'] else self.config['outgoing_ports'][0]
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 = (
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})
self.core.apply_session_settings(
{'outgoing_port': port, 'num_outgoing_ports': num_ports}
)
def _on_set_peer_tos(self, key, value):
try:
@ -233,8 +266,21 @@ class PreferencesManager(component.Component):
log.error('Invalid tos byte: %s', ex)
def _on_set_dht(self, key, value):
dht_bootstraps = 'router.bittorrent.com:6881,router.utorrent.com:6881,router.bitcomet.com:6881'
self.core.apply_session_settings({'dht_bootstrap_nodes': dht_bootstraps, 'enable_dht': value})
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):
self.core.apply_session_setting('enable_upnp', value)
@ -260,12 +306,21 @@ class PreferencesManager(component.Component):
def _on_set_encryption(self, key, value):
# 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}
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})
{
'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):
self.core.apply_session_setting('connections_limit', value)
@ -327,20 +382,29 @@ class PreferencesManager(component.Component):
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):
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 as ex:
log.debug('Network error while trying to send info: %s', ex)
else:
self.config['info_sent'] = now
if value:
SendInfoThread(self.config).start()
@ -352,7 +416,8 @@ class PreferencesManager(component.Component):
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:
@ -361,31 +426,34 @@ class PreferencesManager(component.Component):
def _on_set_proxy(self, key, value):
# Initialise with type none and blank hostnames.
proxy_settings = {
'proxy_type': lt.proxy_type.none,
'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']
'anonymous_mode': value['anonymous_mode'],
}
if value['type'] == lt.proxy_type.i2p_proxy:
proxy_settings.update({
'proxy_type': lt.proxy_type.i2p_proxy,
'i2p_hostname': value['hostname'],
'i2p_port': value['port'],
})
elif value['type'] != lt.proxy_type.none:
proxy_settings.update({
'proxy_type': value['type'],
'proxy_hostname': value['hostname'],
'proxy_port': value['port'],
'proxy_username': value['username'],
'proxy_password': value['password'],
})
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)
@ -396,7 +464,9 @@ class PreferencesManager(component.Component):
# Load the GeoIP DB for country look-ups if available
if os.path.exists(geoipdb_path):
try:
self.core.geoip_instance = GeoIP.open(geoipdb_path, GeoIP.GEOIP_STANDARD)
self.core.geoip_instance = GeoIP.open(
geoipdb_path, GeoIP.GEOIP_STANDARD
)
except AttributeError:
log.warning('GeoIP Unavailable')
else:

View File

@ -18,14 +18,25 @@ import traceback
from collections import namedtuple
from types import FunctionType
from OpenSSL import SSL, crypto
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_ADMIN, AUTH_LEVEL_DEFAULT, AUTH_LEVEL_NONE
from deluge.error import DelugeError, IncompatibleClient, NotAuthorizedError, WrappedException, _ClientSideRecreateError
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
@ -47,13 +58,23 @@ 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
@ -91,22 +112,6 @@ def format_request(call):
return s
class ServerContextFactory(object):
def getContext(self): # NOQA: N802
"""
Create an SSL context.
This loads the servers cert/private key SSL files for use with the
SSL transport.
"""
ssl_dir = deluge.configmanager.get_config_dir('ssl')
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
ctx.use_certificate_file(os.path.join(ssl_dir, 'daemon.cert'))
ctx.use_privatekey_file(os.path.join(ssl_dir, 'daemon.pkey'))
return ctx
class DelugeRPCProtocol(DelugeTransferProtocol):
def __init__(self):
super(DelugeRPCProtocol, self).__init__()
@ -134,8 +139,10 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
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))
reactor.callLater(0, self.dispatch, *call)
@ -152,7 +159,7 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
try:
self.transfer_message(data)
except Exception as ex:
log.warn('Error occurred when sending message: %s.', ex)
log.warning('Error occurred when sending message: %s.', ex)
log.exception(ex)
raise
@ -161,11 +168,11 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
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] = self.AuthLevel(AUTH_LEVEL_NONE, '')
self.factory.authorized_sessions[self.transport.sessionno] = self.AuthLevel(
AUTH_LEVEL_NONE, ''
)
def connectionLost(self, reason=connectionDone): # NOQA: N802
"""
@ -184,7 +191,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
del self.factory.interested_events[self.transport.sessionno]
if self.factory.state == 'running':
component.get('EventManager').emit(ClientDisconnectedEvent(self.factory.session_id))
component.get('EventManager').emit(
ClientDisconnectedEvent(self.factory.session_id)
)
log.info('Deluge client disconnected: %s', reason.value)
def valid_session(self):
@ -206,32 +215,42 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
:type kwargs: dict
"""
def send_error():
"""
Sends an error response with the contents of the exception that was raised.
"""
exceptionType, exceptionValue, dummy_exceptionTraceback = sys.exc_info()
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
))
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)
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)
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)
log.error(
'An exception occurred while sending RPC_ERROR to client: %s', ex
)
if method == 'daemon.info':
# This is a special case and used in the initial connection process
@ -248,7 +267,8 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
ret = component.get('AuthManager').authorize(*args, **kwargs)
if ret:
self.factory.authorized_sessions[
self.transport.sessionno] = self.AuthLevel(ret, args[0])
self.transport.sessionno
] = self.AuthLevel(ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self
except Exception as ex:
send_error()
@ -290,11 +310,15 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
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
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)
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.
@ -310,6 +334,7 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
# 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))
@ -379,8 +404,13 @@ class RPCServer(component.Component):
# 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)
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
@ -526,18 +556,35 @@ 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'
@ -564,6 +611,7 @@ def generate_ssl_keys():
This method generates a new SSL key/cert.
"""
from deluge.common import PY2
digest = 'sha256' if not PY2 else b'sha256'
# Generate key pair

View File

@ -28,7 +28,11 @@ from deluge.common import decode_bytes
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.decorators import deprecated
from deluge.event import TorrentFolderRenamedEvent, TorrentStateChangedEvent, TorrentTrackerStatusEvent
from deluge.event import (
TorrentFolderRenamedEvent,
TorrentStateChangedEvent,
TorrentTrackerStatusEvent,
)
try:
from urllib.parse import urlparse
@ -52,7 +56,7 @@ LT_TORRENT_STATE_MAP = {
'finished': 'Seeding',
'seeding': 'Seeding',
'allocating': 'Allocating',
'checking_resume_data': 'Checking'
'checking_resume_data': 'Checking',
}
@ -65,6 +69,7 @@ def sanitize_filepath(filepath, folder=False):
Args:
folder (bool): A trailing slash is appended to the returned filepath.
"""
def clean_filename(filename):
"""Strips whitespace and discards dotted filenames"""
filename = filename.strip()
@ -110,12 +115,14 @@ def convert_lt_files(files):
except AttributeError:
file_path = _file.path
filelist.append({
'index': index,
'path': file_path.replace('\\', '/'),
'size': _file.size,
'offset': _file.offset
})
filelist.append(
{
'index': index,
'path': file_path.replace('\\', '/'),
'size': _file.size,
'offset': _file.offset,
}
)
return filelist
@ -128,7 +135,7 @@ class TorrentOptions(dict):
auto_managed (bool): Set torrent to auto managed mode, i.e. will be started or queued automatically.
download_location (str): The path for the torrent data to be stored while downloading.
file_priorities (list of int): The priority for files in torrent, range is [0..7] however
only [0, 1, 5, 7] are normally used and correspond to [Do Not Download, Normal, High, Highest]
only [0, 1, 4, 7] are normally used and correspond to [Skip, Low, Normal, High]
mapped_files (dict): A mapping of the renamed filenames in 'index:filename' pairs.
max_connections (int): Sets maximum number of connections this torrent will open.
This must be at least 2. The default is unlimited (-1).
@ -152,6 +159,7 @@ class TorrentOptions(dict):
stop_ratio (float): The seeding ratio to stop (or remove) the torrent at.
super_seeding (bool): Enable super seeding/initial seeding.
"""
def __init__(self):
super(TorrentOptions, self).__init__()
config = ConfigManager('core.conf').config
@ -172,7 +180,7 @@ class TorrentOptions(dict):
'shared': 'shared',
'stop_at_ratio': 'stop_seed_at_ratio',
'stop_ratio': 'stop_seed_ratio',
'super_seeding': 'super_seeding'
'super_seeding': 'super_seeding',
}
for opt_k, conf_k in options_conf_map.items():
self[opt_k] = config[conf_k]
@ -227,6 +235,7 @@ class Torrent(object):
we can re-pause it after its done if necessary
forced_error (TorrentError): Keep track if we have forced this torrent to be in Error state.
"""
def __init__(self, handle, options, state=None, filename=None, magnet=None):
self.torrent_id = str(handle.info_hash())
if log.isEnabledFor(logging.DEBUG):
@ -237,7 +246,6 @@ class Torrent(object):
self.rpcserver = component.get('RPCServer')
self.handle = handle
self.handle.resolve_countries(True)
self.magnet = magnet
self.status = self.handle.status()
@ -296,7 +304,9 @@ class Torrent(object):
# Skip set_prioritize_first_last if set_file_priorities is in options as it also calls the method.
if 'file_priorities' in options and 'prioritize_first_last_pieces' in options:
self.options['prioritize_first_last_pieces'] = options.pop('prioritize_first_last_pieces')
self.options['prioritize_first_last_pieces'] = options.pop(
'prioritize_first_last_pieces'
)
for key, value in options.items():
if key in self.options:
@ -408,8 +418,12 @@ class Torrent(object):
# Set the pieces in first and last ranges to priority 7
# if they are not marked as do not download
priorities[first_start:first_end] = [p and 7 for p in priorities[first_start:first_end]]
priorities[last_start:last_end] = [p and 7 for p in priorities[last_start:last_end]]
priorities[first_start:first_end] = [
p and 7 for p in priorities[first_start:first_end]
]
priorities[last_start:last_end] = [
p and 7 for p in priorities[last_start:last_end]
]
# Setting the priorites for all the pieces of this torrent
self.handle.prioritize_pieces(priorities)
@ -440,11 +454,8 @@ class Torrent(object):
Args:
super_seeding (bool): Enable super seeding.
"""
if self.status.is_seeding:
self.options['super_seeding'] = super_seeding
self.handle.super_seeding(super_seeding)
else:
self.options['super_seeding'] = False
self.options['super_seeding'] = super_seeding
self.handle.super_seeding(super_seeding)
def set_stop_ratio(self, stop_ratio):
"""The seeding ratio to stop (or remove) the torrent at.
@ -493,32 +504,35 @@ class Torrent(object):
Args:
file_priorities (list of int): List of file priorities.
"""
if not self.has_metadata:
return
if log.isEnabledFor(logging.DEBUG):
log.debug('Setting %s file priorities to: %s', self.torrent_id, file_priorities)
log.debug(
'Setting %s file priorities to: %s', self.torrent_id, file_priorities
)
if (self.handle.has_metadata() and file_priorities and
len(file_priorities) == len(self.get_files())):
if file_priorities and len(file_priorities) == len(self.get_files()):
self.handle.prioritize_files(file_priorities)
else:
log.debug('Unable to set new file priorities.')
file_priorities = self.handle.file_priorities()
if 0 in self.options['file_priorities']:
# Previously marked a file 'Do Not Download' so check if changed any 0's to >0.
# Previously marked a file 'skip' so check for any 0's now >0.
for index, priority in enumerate(self.options['file_priorities']):
if priority == 0 and file_priorities[index] > 0:
# Changed 'Do Not Download' to a download priority so update state.
# Changed priority from skip to download so update state.
self.is_finished = False
self.update_state()
break
# Ensure stored options are in sync in case file_priorities were faulty (old state?).
self.options['file_priorities'] = self.handle.file_priorities()
# Store the priorities.
self.options['file_priorities'] = file_priorities
# Set the first/last priorities if needed.
if self.options['prioritize_first_last_pieces']:
self.set_prioritize_first_last_pieces(self.options['prioritize_first_last_pieces'])
self.set_prioritize_first_last_pieces(True)
@deprecated
def set_save_path(self, download_location):
@ -594,11 +608,16 @@ class Torrent(object):
if self.tracker_status != status:
self.tracker_status = status
component.get('EventManager').emit(TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status))
component.get('EventManager').emit(
TorrentTrackerStatusEvent(self.torrent_id, self.tracker_status)
)
def merge_trackers(self, torrent_info):
"""Merges new trackers in torrent_info into torrent"""
log.info('Adding any new trackers to torrent (%s) already in session...', self.torrent_id)
log.info(
'Adding any new trackers to torrent (%s) already in session...',
self.torrent_id,
)
if not torrent_info:
return
# Don't merge trackers if either torrent has private flag set.
@ -636,13 +655,23 @@ class Torrent(object):
self.state = LT_TORRENT_STATE_MAP.get(str(status.state), str(status.state))
if self.state != old_state:
component.get('EventManager').emit(TorrentStateChangedEvent(self.torrent_id, self.state))
component.get('EventManager').emit(
TorrentStateChangedEvent(self.torrent_id, self.state)
)
if log.isEnabledFor(logging.DEBUG):
log.debug('State from lt was: %s | Session is paused: %s\nTorrent state set from "%s" to "%s" (%s)',
'error' if status_error else status.state, session_paused, old_state, self.state, self.torrent_id)
log.debug(
'State from lt was: %s | Session is paused: %s\nTorrent state set from "%s" to "%s" (%s)',
'error' if status_error else status.state,
session_paused,
old_state,
self.state,
self.torrent_id,
)
if self.forced_error:
log.debug('Torrent Error state message: %s', self.forced_error.error_message)
log.debug(
'Torrent Error state message: %s', self.forced_error.error_message
)
def set_status_message(self, message=None):
"""Sets the torrent status message.
@ -697,16 +726,23 @@ class Torrent(object):
"""
status = self.status
eta = 0
if self.is_finished and self.options['stop_at_ratio'] and status.upload_payload_rate:
if (
self.is_finished
and self.options['stop_at_ratio']
and status.upload_payload_rate
):
# We're a seed, so calculate the time to the 'stop_share_ratio'
eta = ((status.all_time_download * self.options['stop_ratio']) -
status.all_time_upload) // status.upload_payload_rate
eta = (
int(status.all_time_download * self.options['stop_ratio'])
- status.all_time_upload
) // status.upload_payload_rate
elif status.download_payload_rate:
left = status.total_wanted - status.total_wanted_done
if left > 0:
eta = left // status.download_payload_rate
return eta
# Limit to 1 year, avoid excessive values and prevent GTK int overflow.
return eta if eta < 31557600 else -1
def get_ratio(self):
"""Get the ratio of upload/download for this torrent.
@ -777,24 +813,30 @@ class Torrent(object):
client = decode_bytes(peer.client)
try:
country = component.get('Core').geoip_instance.country_code_by_addr(peer.ip[0])
country = component.get('Core').geoip_instance.country_code_by_addr(
peer.ip[0]
)
except AttributeError:
country = ''
else:
try:
country = ''.join([char if char.isalpha() else ' ' for char in country])
country = ''.join(
[char if char.isalpha() else ' ' for char in country]
)
except TypeError:
country = ''
ret.append({
'client': client,
'country': country,
'down_speed': peer.payload_down_speed,
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
'progress': peer.progress,
'seed': peer.flags & peer.seed,
'up_speed': peer.payload_up_speed,
})
ret.append(
{
'client': client,
'country': country,
'down_speed': peer.payload_down_speed,
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
'progress': peer.progress,
'seed': peer.flags & peer.seed,
'up_speed': peer.payload_up_speed,
}
)
return ret
@ -825,8 +867,12 @@ class Torrent(object):
"""
if not self.has_metadata:
return []
return [progress / _file.size if _file.size else 0.0 for progress, _file in
zip(self.handle.file_progress(), self.torrent_info.files())]
return [
progress / _file.size if _file.size else 0.0
for progress, _file in zip(
self.handle.file_progress(), self.torrent_info.files()
)
]
def get_tracker_host(self):
"""Get the hostname of the currently connected tracker.
@ -846,7 +892,7 @@ class Torrent(object):
if tracker:
url = urlparse(tracker.replace('udp://', 'http://'))
if hasattr(url, 'hostname'):
host = (url.hostname or 'DHT')
host = url.hostname or 'DHT'
# Check if hostname is an IP address and just return it if that's the case
try:
socket.inet_aton(host)
@ -881,14 +927,18 @@ class Torrent(object):
str: the name of the torrent.
"""
if not self.options['name']:
handle_name = self.handle.name()
if handle_name:
name = decode_bytes(handle_name)
else:
name = self.torrent_id
if self.options['name']:
return self.options['name']
if self.has_metadata:
# Use the top-level folder as torrent name.
filename = decode_bytes(self.torrent_info.file_at(0).path)
name = filename.replace('\\', '/', 1).split('/', 1)[0]
else:
name = self.options['name']
name = decode_bytes(self.handle.name())
if not name:
name = self.torrent_id
return name
@ -987,7 +1037,9 @@ class Torrent(object):
'seeding_time': lambda: self.status.seeding_time,
'finished_time': lambda: self.status.finished_time,
'all_time_download': lambda: self.status.all_time_download,
'storage_mode': lambda: self.status.storage_mode.name.split('_')[2], # sparse or allocate
'storage_mode': lambda: self.status.storage_mode.name.split('_')[
2
], # sparse or allocate
'distributed_copies': lambda: max(0.0, self.status.distributed_copies),
'download_payload_rate': lambda: self.status.download_payload_rate,
'file_priorities': self.get_file_priorities,
@ -1000,8 +1052,12 @@ class Torrent(object):
'max_upload_slots': lambda: self.options['max_upload_slots'],
'max_upload_speed': lambda: self.options['max_upload_speed'],
'message': lambda: self.statusmsg,
'move_on_completed_path': lambda: self.options['move_completed_path'], # Deprecated: move_completed_path
'move_on_completed': lambda: self.options['move_completed'], # Deprecated: Use move_completed
'move_on_completed_path': lambda: self.options[
'move_completed_path'
], # Deprecated: move_completed_path
'move_on_completed': lambda: self.options[
'move_completed'
], # Deprecated: Use move_completed
'move_completed_path': lambda: self.options['move_completed_path'],
'move_completed': lambda: self.options['move_completed'],
'next_announce': lambda: self.status.next_announce.seconds,
@ -1009,17 +1065,26 @@ class Torrent(object):
'num_seeds': lambda: self.status.num_seeds,
'owner': lambda: self.options['owner'],
'paused': lambda: self.status.paused,
'prioritize_first_last': lambda: self.options['prioritize_first_last_pieces'],
'prioritize_first_last': lambda: self.options[
'prioritize_first_last_pieces'
],
# Deprecated: Use prioritize_first_last_pieces
'prioritize_first_last_pieces': lambda: self.options['prioritize_first_last_pieces'],
'prioritize_first_last_pieces': lambda: self.options[
'prioritize_first_last_pieces'
],
'sequential_download': lambda: self.options['sequential_download'],
'progress': self.get_progress,
'shared': lambda: self.options['shared'],
'remove_at_ratio': lambda: self.options['remove_at_ratio'],
'save_path': lambda: self.options['download_location'], # Deprecated: Use download_location
'save_path': lambda: self.options[
'download_location'
], # Deprecated: Use download_location
'download_location': lambda: self.options['download_location'],
'seeds_peers_ratio': lambda: -1.0 if self.status.num_incomplete == 0 else ( # Use -1.0 to signify infinity
self.status.num_complete / self.status.num_incomplete),
'seeds_peers_ratio': lambda: -1.0
if self.status.num_incomplete == 0
else ( # Use -1.0 to signify infinity
self.status.num_complete / self.status.num_incomplete
),
'seed_rank': lambda: self.status.seed_rank,
'state': lambda: self.state,
'stop_at_ratio': lambda: self.options['stop_at_ratio'],
@ -1032,19 +1097,32 @@ class Torrent(object):
'total_seeds': lambda: self.status.num_complete,
'total_uploaded': lambda: self.status.all_time_upload,
'total_wanted': lambda: self.status.total_wanted,
'total_remaining': lambda: self.status.total_wanted - self.status.total_wanted_done,
'total_remaining': lambda: self.status.total_wanted
- self.status.total_wanted_done,
'tracker': lambda: self.status.current_tracker,
'tracker_host': self.get_tracker_host,
'trackers': lambda: self.trackers,
'tracker_status': lambda: self.tracker_status,
'upload_payload_rate': lambda: self.status.upload_payload_rate,
'comment': lambda: decode_bytes(self.torrent_info.comment()) if self.has_metadata else '',
'creator': lambda: decode_bytes(self.torrent_info.creator()) if self.has_metadata else '',
'num_files': lambda: self.torrent_info.num_files() if self.has_metadata else 0,
'num_pieces': lambda: self.torrent_info.num_pieces() if self.has_metadata else 0,
'piece_length': lambda: self.torrent_info.piece_length() if self.has_metadata else 0,
'comment': lambda: decode_bytes(self.torrent_info.comment())
if self.has_metadata
else '',
'creator': lambda: decode_bytes(self.torrent_info.creator())
if self.has_metadata
else '',
'num_files': lambda: self.torrent_info.num_files()
if self.has_metadata
else 0,
'num_pieces': lambda: self.torrent_info.num_pieces()
if self.has_metadata
else 0,
'piece_length': lambda: self.torrent_info.piece_length()
if self.has_metadata
else 0,
'private': lambda: self.torrent_info.priv() if self.has_metadata else False,
'total_size': lambda: self.torrent_info.total_size() if self.has_metadata else 0,
'total_size': lambda: self.torrent_info.total_size()
if self.has_metadata
else 0,
'eta': self.get_eta,
'file_progress': self.get_file_progress,
'files': self.get_files,
@ -1061,7 +1139,7 @@ class Torrent(object):
'super_seeding': lambda: self.status.super_seeding,
'time_since_download': lambda: self.status.time_since_download,
'time_since_upload': lambda: self.status.time_since_upload,
'time_since_transfer': self.get_time_since_transfer
'time_since_transfer': self.get_time_since_transfer,
}
def pause(self):
@ -1074,30 +1152,35 @@ class Torrent(object):
# Turn off auto-management so the torrent will not be unpaused by lt queueing
self.handle.auto_managed(False)
if self.state == 'Error':
return False
log.debug('Unable to pause torrent while in Error state')
elif self.status.paused:
# This torrent was probably paused due to being auto managed by lt
# Since we turned auto_managed off, we should update the state which should
# show it as 'Paused'. We need to emit a torrent_paused signal because
# the torrent_paused alert from libtorrent will not be generated.
self.update_state()
component.get('EventManager').emit(TorrentStateChangedEvent(self.torrent_id, 'Paused'))
component.get('EventManager').emit(
TorrentStateChangedEvent(self.torrent_id, 'Paused')
)
else:
try:
self.handle.pause()
except RuntimeError as ex:
log.debug('Unable to pause torrent: %s', ex)
return False
return True
def resume(self):
"""Resumes this torrent."""
if self.status.paused and self.status.auto_managed:
log.debug('Resume not possible for auto-managed torrent!')
elif self.forced_error and self.forced_error.was_paused:
log.debug('Resume skipped for forced_error torrent as it was originally paused.')
elif (self.status.is_finished and self.options['stop_at_ratio'] and
self.get_ratio() >= self.options['stop_ratio']):
log.debug(
'Resume skipped for forced_error torrent as it was originally paused.'
)
elif (
self.status.is_finished
and self.options['stop_at_ratio']
and self.get_ratio() >= self.options['stop_ratio']
):
log.debug('Resume skipped for torrent as it has reached "stop_seed_ratio".')
else:
# Check if torrent was originally being auto-managed.
@ -1147,9 +1230,13 @@ class Torrent(object):
try:
os.makedirs(dest)
except OSError as ex:
log.error('Could not move storage for torrent %s since %s does '
'not exist and could not create the directory: %s',
self.torrent_id, dest, ex)
log.error(
'Could not move storage for torrent %s since %s does '
'not exist and could not create the directory: %s',
self.torrent_id,
dest,
ex,
)
return False
try:
@ -1182,8 +1269,9 @@ class Torrent(object):
flags = lt.save_resume_flags_t.flush_disk_cache if flush_disk_cache else 0
# Don't generate fastresume data if torrent is in a Deluge Error state.
if self.forced_error:
component.get('TorrentManager').waiting_on_resume_data[self.torrent_id].errback(
UserWarning('Skipped creating resume_data while in Error state'))
component.get('TorrentManager').waiting_on_resume_data[
self.torrent_id
].errback(UserWarning('Skipped creating resume_data while in Error state'))
else:
self.handle.save_resume_data(flags)
@ -1205,12 +1293,11 @@ class Torrent(object):
log.error('Unable to save torrent file to: %s', ex)
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
# Regenerate the file priorities
self.set_file_priorities([])
if filedump is None:
metadata = lt.bdecode(self.torrent_info.metadata())
torrent_file = {b'info': metadata}
filedump = lt.bencode(torrent_file)
lt_ct = lt.create_torrent(self.torrent_info)
filedump = lt.bencode(lt_ct.generate())
write_file(filepath, filedump)
# If the user has requested a copy of the torrent be saved elsewhere we need to do that.
@ -1222,9 +1309,13 @@ class Torrent(object):
def delete_torrentfile(self, delete_copies=False):
"""Deletes the .torrent file in the state directory in config"""
torrent_files = [os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')]
torrent_files = [
os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
]
if delete_copies:
torrent_files.append(os.path.join(self.config['torrentfiles_location'], self.filename))
torrent_files.append(
os.path.join(self.config['torrentfiles_location'], self.filename)
)
for torrent_file in torrent_files:
log.debug('Deleting torrent file: %s', torrent_file)
@ -1284,7 +1375,7 @@ class Torrent(object):
# lt needs utf8 byte-string. Otherwise if wstrings enabled, unicode string.
try:
self.handle.rename_file(index, filename.encode('utf8'))
except TypeError:
except (UnicodeDecodeError, TypeError):
self.handle.rename_file(index, filename)
def rename_folder(self, folder, new_folder):
@ -1320,15 +1411,19 @@ class Torrent(object):
new_path = _file['path'].replace(folder, new_folder, 1)
try:
self.handle.rename_file(_file['index'], new_path.encode('utf8'))
except TypeError:
except (UnicodeDecodeError, TypeError):
self.handle.rename_file(_file['index'], new_path)
def on_folder_rename_complete(dummy_result, torrent, folder, new_folder):
"""Folder rename complete"""
component.get('EventManager').emit(TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder))
component.get('EventManager').emit(
TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder)
)
# Empty folders are removed after libtorrent folder renames
self.remove_empty_folders(folder)
torrent.waiting_on_folder_rename = [_dir for _dir in torrent.waiting_on_folder_rename if _dir]
torrent.waiting_on_folder_rename = [
_dir for _dir in torrent.waiting_on_folder_rename if _dir
]
component.get('TorrentManager').save_resume_data((self.torrent_id,))
d = DeferredList(list(wait_on_folder.values()))
@ -1345,7 +1440,9 @@ class Torrent(object):
"""
# Removes leading slashes that can cause join to ignore download_location
download_location = self.options['download_location']
folder_full_path = os.path.normpath(os.path.join(download_location, folder.lstrip('\\/')))
folder_full_path = os.path.normpath(
os.path.join(download_location, folder.lstrip('\\/'))
)
try:
if not os.listdir(folder_full_path):
@ -1356,7 +1453,9 @@ class Torrent(object):
for name in dirs:
try:
os.removedirs(os.path.join(root, name))
log.debug('Removed Empty Folder %s', os.path.join(root, name))
log.debug(
'Removed Empty Folder %s', os.path.join(root, name)
)
except OSError as ex:
log.debug(ex)
@ -1379,16 +1478,24 @@ class Torrent(object):
pieces = None
else:
pieces = []
for piece, avail_piece in zip(self.status.pieces, self.handle.piece_availability()):
for piece, avail_piece in zip(
self.status.pieces, self.handle.piece_availability()
):
if piece:
pieces.append(3) # Completed.
elif avail_piece:
pieces.append(1) # Available, just not downloaded nor being downloaded.
pieces.append(
1
) # Available, just not downloaded nor being downloaded.
else:
pieces.append(0) # Missing, no known peer with piece, or not asked for yet.
pieces.append(
0
) # Missing, no known peer with piece, or not asked for yet.
for peer_info in self.handle.get_peer_info():
if peer_info.downloading_piece_index >= 0:
pieces[peer_info.downloading_piece_index] = 2 # Being downloaded from peer.
pieces[
peer_info.downloading_piece_index
] = 2 # Being downloaded from peer.
return pieces

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

@ -23,11 +23,14 @@ 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
@ -57,6 +60,7 @@ def overrides(*args):
# called with the real function as argument
def ret_func(func, **kwargs):
return _overrides(stack, func, explicit_base_classes=args)
return ret_func
@ -75,7 +79,10 @@ def _overrides(stack, method, explicit_base_classes=None):
check_classes = base_classes
if not base_classes:
raise ValueError('overrides decorator: unable to determine base class of class "%s"' % class_name)
raise ValueError(
'overrides decorator: unable to determine base class of class "%s"'
% class_name
)
def get_class(cls_name):
if '.' not in cls_name:
@ -91,7 +98,9 @@ def _overrides(stack, method, explicit_base_classes=None):
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 = 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
@ -101,21 +110,36 @@ def _overrides(stack, method, explicit_base_classes=None):
# 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))
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])))
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])))
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
@ -129,8 +153,11 @@ def deprecated(func):
@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.warn(
'Call to deprecated function {}.'.format(func.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter('default', DeprecationWarning) # Reset filter
return func(*args, **kwargs)

View File

@ -13,7 +13,6 @@ from __future__ import unicode_literals
class DelugeError(Exception):
def __new__(cls, *args, **kwargs):
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
inst._args = args
@ -45,7 +44,6 @@ class InvalidPathError(DelugeError):
class WrappedException(DelugeError):
def __init__(self, message, exception_type, traceback):
super(WrappedException, self).__init__(message)
self.type = exception_type
@ -60,27 +58,27 @@ class _ClientSideRecreateError(DelugeError):
class IncompatibleClient(_ClientSideRecreateError):
def __init__(self, daemon_version):
self.daemon_version = daemon_version
msg = 'Your deluge client is not compatible with the daemon. '\
'Please upgrade your client to %(daemon_version)s' % \
dict(daemon_version=self.daemon_version)
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):
msg = '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 __init__(self, message, username):
super(_UsernameBasedPasstroughError, self).__init__(message)
self.username = username

View File

@ -16,6 +16,8 @@ and subsequently emitted to the clients.
"""
from __future__ import unicode_literals
import six
known_events = {}
@ -23,13 +25,14 @@ class DelugeEventMetaClass(type):
"""
This metaclass simply keeps a list of all events classes created.
"""
def __init__(self, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super(DelugeEventMetaClass, self).__init__(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':
known_events[name] = self
known_events[name] = cls
class DelugeEvent(object):
class DelugeEvent(six.with_metaclass(DelugeEventMetaClass, object)):
"""
The base class for all events.
@ -39,7 +42,6 @@ class DelugeEvent(object):
:type args: list
"""
__metaclass__ = DelugeEventMetaClass
def _get_name(self):
return self.__class__.__name__
@ -57,6 +59,7 @@ 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
@ -71,6 +74,7 @@ class TorrentRemovedEvent(DelugeEvent):
"""
Emitted when a torrent has been removed from the session.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -83,6 +87,7 @@ 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
@ -95,6 +100,7 @@ class TorrentStateChangedEvent(DelugeEvent):
"""
Emitted when a torrent changes state.
"""
def __init__(self, torrent_id, state):
"""
:param torrent_id: the torrent_id
@ -109,6 +115,7 @@ class TorrentTrackerStatusEvent(DelugeEvent):
"""
Emitted when a torrents tracker status changes.
"""
def __init__(self, torrent_id, status):
"""
Args:
@ -122,6 +129,7 @@ class TorrentQueueChangedEvent(DelugeEvent):
"""
Emitted when the queue order has changed.
"""
pass
@ -129,6 +137,7 @@ 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
@ -145,6 +154,7 @@ 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
@ -161,6 +171,7 @@ class TorrentFinishedEvent(DelugeEvent):
"""
Emitted when a torrent finishes downloading.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -173,6 +184,7 @@ class TorrentResumedEvent(DelugeEvent):
"""
Emitted when a torrent resumes from a paused state.
"""
def __init__(self, torrent_id):
"""
:param torrent_id: the torrent_id
@ -185,6 +197,7 @@ class TorrentFileCompletedEvent(DelugeEvent):
"""
Emitted when a file completes.
"""
def __init__(self, torrent_id, index):
"""
:param torrent_id: the torrent_id
@ -199,6 +212,7 @@ 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
@ -213,6 +227,7 @@ class CreateTorrentProgressEvent(DelugeEvent):
"""
Emitted when creating a torrent file remotely.
"""
def __init__(self, piece_count, num_pieces):
self._args = [piece_count, num_pieces]
@ -221,6 +236,7 @@ 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
@ -234,6 +250,7 @@ class SessionStartedEvent(DelugeEvent):
Emitted when a session has started. This typically only happens once when
the daemon is initially started.
"""
pass
@ -241,6 +258,7 @@ class SessionPausedEvent(DelugeEvent):
"""
Emitted when the session has been paused.
"""
pass
@ -248,6 +266,7 @@ class SessionResumedEvent(DelugeEvent):
"""
Emitted when the session has been resumed.
"""
pass
@ -255,6 +274,7 @@ class ConfigValueChangedEvent(DelugeEvent):
"""
Emitted when a config value changes in the Core.
"""
def __init__(self, key, value):
"""
:param key: the key that changed
@ -268,6 +288,7 @@ class PluginEnabledEvent(DelugeEvent):
"""
Emitted when a plugin is enabled in the Core.
"""
def __init__(self, plugin_name):
self._args = [plugin_name]
@ -276,6 +297,7 @@ class PluginDisabledEvent(DelugeEvent):
"""
Emitted when a plugin is disabled in the Core.
"""
def __init__(self, plugin_name):
self._args = [plugin_name]
@ -284,6 +306,7 @@ class ClientDisconnectedEvent(DelugeEvent):
"""
Emitted when a client disconnects.
"""
def __init__(self, session_id):
self._args = [session_id]
@ -292,6 +315,7 @@ class ExternalIPEvent(DelugeEvent):
"""
Emitted when the external ip address is received from libtorrent.
"""
def __init__(self, external_ip):
"""
Args:

View File

@ -9,127 +9,193 @@
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, utf8_encode_structure
try:
from urllib.parse import urljoin
except ImportError:
# PY2 fallback
from urlparse import urljoin # pylint: disable=ungrouped-imports
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
super(BodyHandler, self).__init__(request, finished)
self.agent = agent
self.finished = finished
self.total_length = length
self.current_length = 0
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.current_length = 0
self.total_length = 0
self.decoder = None
self.value = filename
self.force_filename = force_filename
self.allow_compression = allow_compression
self.code = None
agent = b'Deluge/%s (http://deluge-torrent.org)' % get_version().encode('utf8')
self.decoder = None
client.HTTPDownloader.__init__(self, url, filename, headers=headers, agent=agent)
def request_callback(self, response):
finished = Deferred()
def gotStatus(self, version, status, message): # NOQA: N802
self.code = int(status)
client.HTTPDownloader.gotStatus(self, version, status, message)
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])
def gotHeaders(self, headers): # NOQA: N802
if self.code == http.OK:
if 'content-length' in headers:
self.total_length = int(headers['content-length'][0])
else:
self.total_length = 0
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 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)
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
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.value)[0], new_file_name)
self.filename = 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
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)
)
self.fileName = new_file_name
self.value = new_file_name
return finished
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))
def request(self, method, uri, headers=None, body_producer=None):
"""Issue a new request to the wrapped agent.
return client.HTTPDownloader.gotHeaders(self, headers)
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.
def pagePart(self, data): # NOQA: N802
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)
Returns:
Deferred: The filename of the of the downloaded file.
"""
if headers is None:
headers = Headers()
return client.HTTPDownloader.pagePart(self, 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)
def pageEnd(self): # NOQA: N802
if self.decoder:
data = self.decoder.flush()
self.current_length -= len(data)
self.decoder = None
self.pagePart(data)
return client.HTTPDownloader.pageEnd(self)
d = self.agent.request(
method=method, uri=uri, headers=headers, bodyProducer=body_producer
)
d.addCallback(self.request_callback)
return d
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
@ -137,136 +203,128 @@ def sanitise_filename(filename):
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:
# 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):
"""
Downloads a file from a specific URL and returns a Deferred. A callback
function can be specified to be called as parts are received.
def _download_file(
url,
filename,
callback=None,
headers=None,
force_filename=False,
allow_compression=True,
handle_redirects=True,
):
"""Downloads a file from a specific URL and returns a Deferred.
A callback function can be specified to be called as parts are received.
Args:
url (str): The url to download from
filename (str): The filename to save the file as
callback (func): A function to be called when a part of data is received,
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 us to use the filename specified rather than
one the server may suggest
allow_compression (bool): Allows gzip & deflate decoding
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
Deferred: The filename of the downloaded file.
Raises:
t.w.e.PageRedirect
t.w.e.Error: for all other HTTP response errors
"""
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)
url = url.encode('utf8')
filename = filename.encode('utf8')
headers = utf8_encode_structure(headers) if headers else headers
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
agent = HTTPDownloaderAgent(
agent, filename, callback, force_filename, allow_compression, handle_redirects
)
# In Twisted 13.1.0 _parse() function replaced by _URI class.
# In Twisted 15.0.0 _URI class renamed to URI.
if hasattr(client, '_parse'):
scheme, host, port, dummy_path = client._parse(url)
else:
try:
from twisted.web.client import _URI as URI
except ImportError:
from twisted.web.client import URI
finally:
uri = URI.fromBytes(url)
scheme = uri.scheme
host = uri.host
port = uri.port
# 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]
if scheme == 'https':
from twisted.internet import ssl
# ClientTLSOptions in Twisted >= 14, see ticket #2765 for details on this addition.
try:
from twisted.internet._sslverify import ClientTLSOptions
except ImportError:
ctx_factory = ssl.ClientContextFactory()
else:
class TLSSNIContextFactory(ssl.ClientContextFactory): # pylint: disable=no-init
"""
A custom context factory to add a server name for TLS connections.
"""
def getContext(self): # NOQA: N802
ctx = ssl.ClientContextFactory.getContext(self)
ClientTLSOptions(host, ctx)
return ctx
ctx_factory = TLSSNIContextFactory()
reactor.connectSSL(host, port, factory, ctx_factory)
else:
reactor.connectTCP(host, port, factory)
return factory.deferred
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.
def download_file(
url,
filename,
callback=None,
headers=None,
force_filename=False,
allow_compression=True,
handle_redirects=True,
):
"""Downloads a file from a specific URL and returns a Deferred.
A callback function can be specified to be called as parts are received.
Args:
url (str): The url to download from
filename (str): The filename to save the file as
callback (func): A function to be called when a part of data is received,
it's signature should be: func(data, current_length, total_length)
headers (dict): Any optional headers to send
force_filename (bool): force us to use the filename specified rather than
one the server may suggest
allow_compression (bool): Allows gzip & deflate decoding
handle_redirects (bool): If HTTP redirects should be handled automatically
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
Deferred: The filename of the downloaded file.
Raises:
t.w.e.PageRedirect: Unless handle_redirects=True
t.w.e.Error: for all other HTTP response errors
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):
if failure.check(PageRedirect) and handle_redirects:
new_url = urljoin(url, failure.getErrorMessage().split(' to ')[1])
result = _download_file(new_url, filename, callback=callback, headers=headers,
force_filename=force_filename,
allow_compression=allow_compression)
result.addCallbacks(on_download_success, on_download_fail)
else:
# Log the failure and pass to the caller
log.warning('Error occurred downloading file from "%s": %s',
url, failure.getErrorMessage())
result = failure
log.warning(
'Error occurred downloading file from "%s": %s',
url,
failure.getErrorMessage(),
)
result = failure
return result
d = _download_file(url, filename, callback=callback, headers=headers,
force_filename=force_filename, allow_compression=allow_compression)
d = _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

View File

@ -9,6 +9,12 @@ from __future__ import unicode_literals
# 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'),
@ -107,3 +113,5 @@ LANGUAGES = {
'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

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

@ -0,0 +1,139 @@
# -*- 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():
libintl = ctypes.cdll.LoadLibrary('libintl-8.dll')
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

@ -29,7 +29,9 @@ 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'
else:
DEFAULT_LOGGING_FORMAT = '%%(asctime)s [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
DEFAULT_LOGGING_FORMAT = (
'%%(asctime)s [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s'
)
MAX_LOGGER_NAME_LENGTH = 10
@ -43,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):
@ -88,11 +92,16 @@ class Logging(LoggingLoggerClass):
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
@ -105,12 +114,18 @@ levels = {
'none': logging.CRITICAL,
'debug': logging.DEBUG,
'trace': 5,
'garbage': 1
'garbage': 1,
}
def setup_logger(level='error', filename=None, filemode='w', logrotate=None,
output_stream=sys.stdout, twisted_observer=True):
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.
@ -127,8 +142,8 @@ def setup_logger(level='error', filename=None, filemode='w', logrotate=None,
"""
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)
@ -136,13 +151,14 @@ def setup_logger(level='error', filename=None, filemode='w', logrotate=None,
if filename and logrotate:
handler = logging.handlers.RotatingFileHandler(
filename, maxBytes=logrotate,
backupCount=5, encoding='utf-8'
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_cls = getattr(
logging.handlers, 'WatchedFileHandler', logging.FileHandler
)
handler = handler_cls(filename, mode=filemode, encoding='utf-8')
else:
handler = logging.StreamHandler(stream=output_stream)
@ -150,8 +166,7 @@ def setup_logger(level='error', filename=None, filemode='w', logrotate=None,
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)
@ -186,7 +201,9 @@ class TwistedLoggingObserver(PythonLoggingObserver):
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))
getattr(LoggingLoggerClass, event_dict['log_level'].name)(
log, fmt % (event_dict)
)
else:
PythonLoggingObserver.emit(self, event_dict)
@ -210,12 +227,14 @@ def tweak_logging_levels():
the command line.
"""
from deluge import configmanager
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)
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('#'):
@ -224,7 +243,7 @@ def tweak_logging_levels():
if level not in levels:
continue
log.warn('Setting logger "%s" to logging level "%s"', name, level)
log.warning('Setting logger "%s" to logging level "%s"', name, level)
set_logger_level(level, name)
@ -234,8 +253,8 @@ def set_logger_level(level, logger_name=None):
: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'))
@ -243,15 +262,20 @@ def set_logger_level(level, logger_name=None):
def get_plugin_logger(logger_name):
import warnings
stack = inspect.stack()
stack.pop(0) # The logging call from this module
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)
@ -278,29 +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):
def __getattribute__(self, name):
import warnings
logger_name = 'deluge'
stack = inspect.stack()
stack.pop(0) # The logging call from this module
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)

View File

@ -10,7 +10,6 @@
from __future__ import division, unicode_literals
import os
import sys
from hashlib import sha1 as sha
from deluge.bencode import bencode
@ -19,6 +18,7 @@ from deluge.common import get_path_size, utf8_encode_structure
class InvalidPath(Exception):
"""Raised when an invalid path is supplied."""
pass
@ -28,6 +28,7 @@ class InvalidPieceSize(Exception):
Note:
Piece sizes must be multiples of 16KiB.
"""
pass
@ -43,6 +44,7 @@ class TorrentMetadata(object):
>>> t.save('/tmp/test.torrent')
"""
def __init__(self):
self.__data_path = None
self.__piece_size = 0
@ -67,9 +69,7 @@ class TorrentMetadata(object):
if not self.data_path:
raise InvalidPath('Need to set a data_path!')
torrent = {
'info': {}
}
torrent = {'info': {}}
if self.comment:
torrent['comment'] = self.comment
@ -122,8 +122,10 @@ class TorrentMetadata(object):
# 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):]
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]:
@ -147,17 +149,19 @@ 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:
with open(os.path.join(self.data_path, *path), 'rb') as _file:
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
@ -166,7 +170,7 @@ class TorrentMetadata(object):
# Run the progress function if necessary
if progress:
progress(len(pieces), num_pieces)
buf = ''
buf = b''
else:
break
r = _file.read(piece_size - len(buf))

View File

@ -54,9 +54,22 @@ class RemoteFileProgress(object):
)
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()
@ -82,8 +95,6 @@ def make_meta_file(path, url, piece_length, progress=None, title=None, comment=N
info = makeinfo(path, piece_length, progress, name, content_type, private)
# check_info(info)
h = open(f, 'wb')
data['info'] = info
if title:
data['title'] = title.encode('utf8')
@ -113,9 +124,8 @@ def make_meta_file(path, url, piece_length, progress=None, title=None, comment=N
data['announce-list'] = trackers
data['encoding'] = 'UTF-8'
h.write(bencode(utf8_encode_structure(data)))
h.close()
with open(f, 'wb') as file_:
file_.write(bencode(utf8_encode_structure(data)))
def calcsize(path):
@ -141,6 +151,7 @@ def makeinfo(path, piece_length, progress, name=None, content_type=None, private
totalsize += os.path.getsize(f)
if totalsize >= piece_length:
import math
num_pieces = math.ceil(totalsize / piece_length)
else:
num_pieces = 1
@ -150,25 +161,25 @@ def makeinfo(path, piece_length, progress, name=None, content_type=None, private
size = os.path.getsize(f)
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 = open(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
@ -177,11 +188,13 @@ def makeinfo(path, piece_length, progress, name=None, content_type=None, private
if not name:
name = os.path.split(path)[1]
return {'pieces': b''.join(pieces),
'piece length': piece_length,
'files': fs,
'name': name.encode('utf8'),
'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:
@ -191,27 +204,32 @@ def makeinfo(path, piece_length, progress, name=None, content_type=None, private
pieces = []
p = 0
h = open(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': 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,
return {
'pieces': b''.join(pieces),
'piece length': piece_length,
'length': size,
'name': name,
'private': private}
'content_type': content_type,
'private': private,
}
return {
'pieces': b''.join(pieces),
'piece length': piece_length,
'length': size,
'name': name,
'private': private,
}
def subfiles(d):

View File

@ -12,14 +12,19 @@ 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)
return attribute & (
win32con.FILE_ATTRIBUTE_HIDDEN | win32con.FILE_ATTRIBUTE_SYSTEM
)
except (AttributeError, AssertionError):
return False
@ -49,7 +54,10 @@ def get_completion_paths(args):
def get_subdirs(dirname):
try:
return os.walk(dirname).next()[1]
if PY2:
return os.walk(dirname).__next__[1]
else:
return next(os.walk(dirname))[1]
except StopIteration:
# Invalid dirname
return []

View File

@ -56,7 +56,9 @@ class PluginManagerBase(object):
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')):
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..
@ -92,26 +94,27 @@ class PluginManagerBase(object):
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',
self.pkg_env[name][0].project_name,
self.pkg_env[name][0].version,
self.pkg_env[name][0].location)
log.debug(
'Found plugin: %s %s at %s',
self.pkg_env[name][0].project_name,
self.pkg_env[name][0].version,
self.pkg_env[name][0].location,
)
self.available_plugins.append(self.pkg_env[name][0].project_name)
def enable_plugin(self, plugin_name):
@ -135,19 +138,21 @@ class PluginManagerBase(object):
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()
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.error(
'Unable to instantiate plugin %r from %r!', name, egg.location
)
log.exception(ex)
continue
try:
@ -159,33 +164,47 @@ class PluginManagerBase(object):
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':
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)
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'))
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_d.addCallbacks(
on_started,
on_started_error,
callbackArgs=[instance],
errbackArgs=[instance],
)
return return_d
return defer.succeed(False)
@ -215,7 +234,9 @@ class PluginManagerBase(object):
def on_disabled(result):
ret = True
if isinstance(result, Failure):
log.debug('Error when disabling plugin %s: %s', name, result.getTraceback())
log.debug(
'Error when disabling plugin %s: %s', name, result.getTraceback()
)
ret = False
try:
component.deregister(self.plugins[name].plugin)
@ -239,14 +260,16 @@ class PluginManagerBase(object):
cont_lines = []
# Missing plugin info
if not self.pkg_env[name]:
log.warn('Failed to retrive info for plugin: %s', 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):
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:

View File

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

View File

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

View File

@ -1,23 +0,0 @@
# -*- 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):
return resource_filename('deluge.plugins.autoadd', os.path.join('data', filename))

View File

@ -1,57 +0,0 @@
/*!
* 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.preferences');
/**
* @class Deluge.ux.preferences.AutoAddPage
* @extends Ext.Panel
*/
Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
title: _('AutoAdd'),
layout: 'fit',
border: false,
initComponent: function() {
Deluge.ux.preferences.AutoAddPage.superclass.initComponent.call(this);
fieldset = this.add({
xtype: 'fieldset',
border: false,
title: _('AutoAdd Preferences'),
autoHeight: true,
labelWidth: 1,
defaultType: 'panel'
});
fieldset.add({
border: false,
bodyCfg: {
html: _('<p>The AutoAdd plugin is enabled however there is no WebUI ' +
'preferences page implemented yet for this plugin.</p><br>' +
'<p>In the meantime please use GtkUI preference page to configure this plugin.<p>')
}
});
}
});
Deluge.plugins.AutoAddPlugin = Ext.extend(Deluge.Plugin, {
name: 'AutoAdd',
onDisable: function() {
deluge.preferences.removePage(this.prefsPage);
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.AutoAddPage());
}
});
Deluge.registerPlugin('AutoAdd', Deluge.plugins.AutoAddPlugin);

View File

@ -1,502 +0,0 @@
# -*- 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 gtk
import gtk.glade
import deluge.common
import deluge.component as component
from deluge.plugins.pluginbase import GtkPluginBase
from deluge.ui.client import client
from deluge.ui.gtkui import dialogs
from .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.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.glade.get_widget('seed_mode').set_active(
options.get('seed_mode', 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 spin_id in self.spin_ids + self.spin_int_ids:
self.glade.get_widget(spin_id).set_value(options.get(spin_id, 0))
self.glade.get_widget(spin_id + '_toggle').set_active(options.get(spin_id + '_toggle', False))
for chk_id in self.chk_ids:
self.glade.get_widget(chk_id).set_active(bool(options.get(chk_id, True)))
self.glade.get_widget(chk_id + '_toggle').set_active(options.get(chk_id + '_toggle', False))
if not options.get('add_paused', True):
self.glade.get_widget('isnt_add_paused').set_active(True)
if not options.get('queue_to_top', True):
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:
acc_iter = self.accounts.append()
self.accounts.set_value(
acc_iter, 0, account['username']
)
if account['username'] == owner:
selected_iter = acc_iter
self.glade.get_widget('OwnerCombobox').set_active_iter(selected_iter)
def on_accounts_failure(failure):
log.debug('Failed to get accounts!!! %s', failure)
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, client.get_auth_user())
self.glade.get_widget('OwnerCombobox').set_active(0)
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
def on_labels(labels):
log.debug('Got Labels: %s', labels)
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:
acc_iter = self.accounts.append()
self.accounts.set_value(acc_iter, 0, client.get_auth_user())
self.glade.get_widget('OwnerCombobox').set_active(0)
self.glade.get_widget('OwnerCombobox').set_sensitive(False)
def 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.glade.get_widget(maintoggle + '_toggle'))
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 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.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', 'seed_mode']:
options[key] = self.glade.get_widget(key).get_active()
for spin_id in self.spin_ids:
options[spin_id] = self.glade.get_widget(spin_id).get_value()
options[spin_id + '_toggle'] = self.glade.get_widget(spin_id + '_toggle').get_active()
for spin_int_id in self.spin_int_ids:
options[spin_int_id] = self.glade.get_widget(spin_int_id).get_value_as_int()
options[spin_int_id + '_toggle'] = self.glade.get_widget(spin_int_id + '_toggle').get_active()
for chk_id in self.chk_ids:
options[chk_id] = self.glade.get_widget(chk_id).get_active()
options[chk_id + '_toggle'] = self.glade.get_widget(chk_id + '_toggle').get_active()
if options['copy_torrent_toggle'] and options['path'] == options['copy_torrent']:
raise IncompatibleOption(_('"Watch Folder" directory and "Copy of .torrent'
' files to" directory cannot be the same!'))
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.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.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.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.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

@ -20,19 +20,22 @@ 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 GtkUIPlugin(PluginInitBase):
class Gtk3UIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name)
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

@ -19,5 +19,6 @@ import os.path
from pkg_resources import resource_filename
def get_resource(filename):
return resource_filename('deluge.plugins.extractor', os.path.join('data', 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

@ -15,10 +15,10 @@
from __future__ import unicode_literals
import base64
import logging
import os
import shutil
from base64 import b64encode
from twisted.internet import reactor
from twisted.internet.task import LoopingCall, deferLater
@ -35,10 +35,7 @@ from deluge.plugins.pluginbase import CorePluginBase
log = logging.getLogger(__name__)
DEFAULT_PREFS = {
'watchdirs': {},
'next_id': 1
}
DEFAULT_PREFS = {'watchdirs': {}, 'next_id': 1}
OPTIONS_AVAILABLE = { # option: builtin
@ -64,7 +61,7 @@ OPTIONS_AVAILABLE = { # option: builtin
'add_paused': True,
'queue_to_top': False,
'owner': True,
'seed_mode': True
'seed_mode': True,
}
MAX_NUM_ATTEMPTS = 10
@ -72,6 +69,7 @@ MAX_NUM_ATTEMPTS = 10
class AutoaddOptionsChangedEvent(DelugeEvent):
"""Emitted when the options for the plugin are changed."""
def __init__(self):
pass
@ -90,6 +88,7 @@ class Core(CorePluginBase):
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
)
@ -123,14 +122,10 @@ class Core(CorePluginBase):
"""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.')
)
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.')
)
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.')
@ -210,8 +205,7 @@ class Core(CorePluginBase):
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)
log.debug('Watchdir id %s is not enabled. Disabling it.', watchdir_id)
self.disable_watchdir(watchdir_id)
return
@ -228,7 +222,10 @@ class Core(CorePluginBase):
# 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']:
if watchdir.get(option + '_toggle', True) or option in [
'owner',
'seed_mode',
]:
options[option] = value
# Check for .magnet files containing multiple magnet links and
@ -237,79 +234,78 @@ class Core(CorePluginBase):
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)
log.error(
'Unable to auto add torrent due to improper filename encoding: %s',
ex,
)
continue
if os.path.isdir(filepath):
# Skip directories
continue
elif os.path.splitext(filename)[1] == '.magnet' and self.split_magnets(filepath):
elif os.path.splitext(filename)[1] == '.magnet' and self.split_magnets(
filepath
):
os.remove(filepath)
for filename in os.listdir(watchdir['abspath']):
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)
log.error(
'Unable to auto add torrent due to improper filename encoding: %s',
ex,
)
continue
if os.path.isdir(filepath):
# Skip directories
continue
else:
ext = os.path.splitext(filename)[1].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
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:
# The torrent looks good, so lets add it to the session.
if magnet:
torrent_id = component.get('Core').add_torrent_magnet(
filedump.strip(), options)
else:
torrent_id = component.get('Core').add_torrent_file(
filename, base64.encodestring(filedump), options)
except AddTorrentError as ex:
log.error(ex)
os.rename(filepath, filepath + '.invalid')
continue
# If the torrent added successfully, set the extra options.
if torrent_id:
if 'Label' in component.get('CorePluginManager').get_enabled_plugins():
if 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:
log.debug('invalid magnet link')
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')
continue
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'):
@ -319,18 +315,48 @@ class Core(CorePluginBase):
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)
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)
log.error(
'Disabling "%s", error during update: %s',
self.watchdirs[watchdir_id]['path'],
failure,
)
@export
def enable_watchdir(self, watchdir_id):
@ -377,12 +403,13 @@ class Core(CorePluginBase):
@export
def get_watchdirs(self):
rpcserver = component.get('RPCServer')
session_user = rpcserver.get_session_user()
session_auth_level = rpcserver.get_session_auth_level()
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)
log.debug(
'Current logged in user %s is an ADMIN, send all ' 'watchdirs',
session_user,
)
return self.watchdirs
watchdirs = {}
@ -390,8 +417,12 @@ class Core(CorePluginBase):
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))
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):
@ -412,7 +443,7 @@ class Core(CorePluginBase):
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.'
'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.')
@ -431,7 +462,9 @@ class Core(CorePluginBase):
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)
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]
@ -447,9 +480,11 @@ class Core(CorePluginBase):
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)
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():
@ -464,9 +499,24 @@ class Core(CorePluginBase):
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)
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)
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

@ -24,8 +24,12 @@ log = logging.getLogger(__name__)
class WebUI(WebPluginBase):
scripts = [get_resource('autoadd.js')]
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

View File

@ -18,12 +18,12 @@ 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.06'
__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__,
@ -35,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__,
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,4 +0,0 @@
from __future__ import unicode_literals
# this is a namespace package
__import__('pkg_resources').declare_namespace(__name__)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -15,6 +15,7 @@ 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)
@ -22,6 +23,7 @@ class CorePlugin(PluginInitBase):
class GtkUIPlugin(PluginInitBase):
def __init__(self, plugin_name):
from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name)
@ -29,5 +31,6 @@ class GtkUIPlugin(PluginInitBase):
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

@ -18,11 +18,12 @@ import os.path
from functools import wraps
from sys import exc_info
import six
from pkg_resources import resource_filename
def get_resource(filename):
return resource_filename('deluge.plugins.blocklist', os.path.join('data', filename))
return resource_filename(__package__, os.path.join('data', filename))
def raises_errors_as(error):
@ -30,8 +31,10 @@ def raises_errors_as(error):
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.
@ -42,10 +45,12 @@ def raises_errors_as(error):
"""
try:
return func(self, *args, **kwargs)
except:
except Exception:
(value, tb) = exc_info()[1:]
raise error, value, tb
six.reraise(error, value, tb)
return wrapper
return decorator
@ -116,37 +121,37 @@ class IP(object):
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 is not 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 is not a previous IP address'))
# q1 -= 1
# else:
# q2 -= 1
# else:
# q3 -= 1
# else:
# q4 -= 1
# return IP(q1, q2, q3, 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 is not 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 is not 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, ''.__class__):
@ -165,5 +170,7 @@ class IP(object):
def __repr__(self):
return '<%s long=%s address="%s">' % (
self.__class__.__name__, self.long, self.address
self.__class__.__name__,
self.long,
self.address,
)

View File

@ -76,11 +76,15 @@ class Core(CorePluginBase):
self.file_progress = 0.0
self.core = component.get('Core')
self.config = deluge.configmanager.ConfigManager('blocklist.conf', DEFAULT_PREFS)
self.config = deluge.configmanager.ConfigManager(
'blocklist.conf', DEFAULT_PREFS
)
if 'whitelisted' not in self.config:
self.config['whitelisted'] = []
self.reader = create_reader(self.config['list_type'], self.config['list_compression'])
self.reader = create_reader(
self.config['list_type'], self.config['list_compression']
)
if not isinstance(self.config['last_update'], float):
self.config.config['last_update'] = 0.0
@ -91,10 +95,15 @@ class Core(CorePluginBase):
if self.config['last_update']:
last_update = datetime.fromtimestamp(self.config['last_update'])
check_period = timedelta(days=self.config['check_after_days'])
if not self.config['last_update'] or last_update + check_period < datetime.now():
if (
not self.config['last_update']
or last_update + check_period < datetime.now()
):
update_now = True
else:
d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache'))
d = self.import_list(
deluge.configmanager.get_config_dir('blocklist.cache')
)
d.addCallbacks(self.on_import_complete, self.on_import_error)
if self.need_to_resume_session:
d.addBoth(self.resume_session)
@ -217,7 +226,10 @@ class Core(CorePluginBase):
if self.config['last_update']:
last_update = datetime.fromtimestamp(self.config['last_update'])
check_period = timedelta(days=self.config['check_after_days'])
if not self.config['last_update'] or last_update + check_period < datetime.now():
if (
not self.config['last_update']
or last_update + check_period < datetime.now()
):
update_now = True
if self.update_timer.running:
self.update_timer.stop()
@ -229,9 +241,11 @@ class Core(CorePluginBase):
self.config[key] = config[key]
if needs_blocklist_import:
log.debug('IP addresses were removed from the whitelist. Since we '
'do not know if they were blocked before. Re-import '
'current blocklist and re-add whitelisted.')
log.debug(
'IP addresses were removed from the whitelist. Since we '
'do not know if they were blocked before. Re-import '
'current blocklist and re-add whitelisted.'
)
self.has_imported = False
d = self.import_list(deluge.configmanager.get_config_dir('blocklist.cache'))
d.addCallbacks(self.on_import_complete, self.on_import_error)
@ -293,6 +307,7 @@ class Core(CorePluginBase):
Deferred: a Deferred which fires once the blocklist has been downloaded.
"""
def on_retrieve_data(data, current_length, total_length):
if total_length:
fp = current_length / total_length
@ -304,6 +319,7 @@ class Core(CorePluginBase):
self.file_progress = fp
import socket
socket.setdefaulttimeout(self.config['timeout'])
if not url:
@ -311,14 +327,18 @@ class Core(CorePluginBase):
headers = {}
if self.config['last_update'] and not self.force_download:
headers['If-Modified-Since'] = formatdate(self.config['last_update'], usegmt=True)
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
url,
deluge.configmanager.get_config_dir('blocklist.download'),
on_retrieve_data,
headers,
)
def on_download_complete(self, blocklist):
@ -365,8 +385,11 @@ class Core(CorePluginBase):
else:
log.warning('Blocklist download failed: %s', error_msg)
if self.failed_attempts < self.config['try_times']:
log.debug('Try downloading blocklist again... (%s/%s)',
self.failed_attempts, self.config['try_times'])
log.debug(
'Try downloading blocklist again... (%s/%s)',
self.failed_attempts,
self.config['try_times'],
)
self.failed_attempts += 1
d = self.download_list()
d.addCallbacks(self.on_download_complete, self.on_download_error)
@ -426,7 +449,11 @@ class Core(CorePluginBase):
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(
'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)
@ -504,13 +531,21 @@ class Core(CorePluginBase):
"""
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'])
self.config['list_type'] = detect_format(
blocklist, self.config['list_compression']
)
log.debug(
'Auto-detected type: %s compression: %s',
self.config['list_type'],
self.config['list_compression'],
)
if not self.config['list_type']:
self.config['list_compression'] = ''
raise UnknownFormatError
else:
self.reader = create_reader(self.config['list_type'], self.config['list_compression'])
self.reader = create_reader(
self.config['list_type'], self.config['list_compression']
)
def pause_session(self):
self.need_to_resume_session = not self.core.session.is_paused()

View File

@ -1,4 +1,4 @@
/*!
/**
* blocklist.js
*
* Copyright (C) Omar Alvarez 2014 <omar.alvarez@udc.es>
@ -16,7 +16,6 @@ Ext.ns('Deluge.ux.preferences');
* @extends Ext.Panel
*/
Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
title: _('Blocklist'),
header: false,
layout: 'fit',
@ -34,14 +33,14 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
defaultType: 'textfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 40
labelWidth: 40,
});
this.URL = this.URLFset.add({
fieldLabel: _('URL:'),
labelSeparator: '',
name: 'url',
width: '80%'
width: '80%',
});
this.SettingsFset = this.add({
@ -52,7 +51,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
defaultType: 'spinnerfield',
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 160
labelWidth: 160,
});
this.checkListDays = this.SettingsFset.add({
@ -61,13 +60,13 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
name: 'check_list_days',
value: 4,
decimalPrecision: 0,
width: 80
width: 80,
});
this.chkImportOnStart = this.SettingsFset.add({
xtype: 'checkbox',
fieldLabel: _('Import blocklist on startup'),
name: 'check_import_startup'
name: 'check_import_startup',
});
this.OptionsFset = this.add({
@ -79,7 +78,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: false,
width: '80%',
labelWidth: 0
labelWidth: 0,
});
this.checkDownload = this.OptionsFset.add({
@ -88,18 +87,21 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
xtype: 'container',
layout: 'hbox',
margins: '4 0 0 5',
items: [{
items: [
{
xtype: 'button',
text: ' Check Download and Import ',
scale: 'medium'
}, {
scale: 'medium',
},
{
xtype: 'box',
autoEl: {
tag: 'img',
src: '../icons/ok.png'
src: '../icons/ok.png',
},
margins: '4 0 0 3'
}]
margins: '4 0 0 3',
},
],
});
this.forceDownload = this.OptionsFset.add({
@ -108,7 +110,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
text: ' Force Download and Import ',
margins: '2 0 0 0',
//icon: '../icons/blocklist_import24.png',
scale: 'medium'
scale: 'medium',
});
this.ProgressFset = this.add({
@ -120,13 +122,13 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
style: 'margin-top: 1px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 0,
hidden: true
hidden: true,
});
this.downProgBar = this.ProgressFset.add({
fieldLabel: _(''),
name: 'progress_bar',
width: '90%'
width: '90%',
});
this.InfoFset = this.add({
@ -136,31 +138,31 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
autoHeight: true,
defaultType: 'label',
style: 'margin-top: 0px; margin-bottom: 0px; padding-bottom: 0px;',
labelWidth: 60
labelWidth: 60,
});
this.lblFileSize = this.InfoFset.add({
fieldLabel: _('File Size:'),
labelSeparator: '',
name: 'file_size'
name: 'file_size',
});
this.lblDate = this.InfoFset.add({
fieldLabel: _('Date:'),
labelSeparator: '',
name: 'date'
name: 'date',
});
this.lblType = this.InfoFset.add({
fieldLabel: _('Type:'),
labelSeparator: '',
name: 'type'
name: 'type',
});
this.lblURL = this.InfoFset.add({
fieldLabel: _('URL:'),
labelSeparator: '',
name: 'lbl_URL'
name: 'lbl_URL',
});
this.WhitelistFset = this.add({
@ -172,7 +174,8 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
style: 'margin-top: 3px; margin-bottom: 0px; padding-bottom: 0px;',
autoWidth: true,
labelWidth: 0,
items: [{
items: [
{
fieldLabel: _(''),
name: 'whitelist',
margins: '2 0 5 5',
@ -181,10 +184,11 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
autoExpandColumn: 'ip',
viewConfig: {
emptyText: _('Add an IP...'),
deferEmptyText: false
deferEmptyText: false,
},
colModel: new Ext.grid.ColumnModel({
columns: [{
columns: [
{
id: 'ip',
header: _('IP'),
dataIndex: 'ip',
@ -192,29 +196,30 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
hideable: false,
editable: true,
editor: {
xtype: 'textfield'
}
}]
xtype: 'textfield',
},
},
],
}),
selModel: new Ext.grid.RowSelectionModel({
singleSelect: false,
moveEditorOnEnter: false
moveEditorOnEnter: false,
}),
store: new Ext.data.ArrayStore({
autoDestroy: true,
fields: [{name: 'ip'}]
fields: [{ name: 'ip' }],
}),
listeners: {
afteredit: function(e) {
e.record.commit();
}
},
},
setEmptyText: function(text) {
if (this.viewReady) {
this.getView().emptyText = text;
this.getView().refresh();
} else {
Ext.apply(this.viewConfig, {emptyText: text});
Ext.apply(this.viewConfig, { emptyText: text });
}
},
loadData: function(data) {
@ -222,28 +227,32 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
if (this.viewReady) {
this.getView().updateHeaders();
}
}
}]
},
},
],
});
this.ipButtonsContainer = this.WhitelistFset.add({
xtype: 'container',
layout: 'hbox',
margins: '4 0 0 5',
items: [{
items: [
{
xtype: 'button',
text: ' Add IP ',
margins: '0 5 0 0'
},{
margins: '0 5 0 0',
},
{
xtype: 'button',
text: ' Delete IP '
}]
text: ' Delete IP ',
},
],
});
this.updateTask = Ext.TaskMgr.start({
interval: 2000,
run: this.onUpdate,
scope: this
scope: this,
});
this.on('show', this.updateConfig, this);
@ -290,8 +299,13 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
this.forceDownload.setDisabled(true);
this.ProgressFset.show();
this.downProgBar.updateProgress(status['file_progress'],'Downloading '.concat((status['file_progress'] * 100).toFixed(2)).concat('%'),true);
this.downProgBar.updateProgress(
status['file_progress'],
'Downloading '
.concat((status['file_progress'] * 100).toFixed(2))
.concat('%'),
true
);
} else if (status['state'] == 'Importing') {
this.InfoFset.hide();
this.checkDownload.getComponent(0).setDisabled(true);
@ -299,8 +313,9 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
this.forceDownload.setDisabled(true);
this.ProgressFset.show();
this.downProgBar.updateText('Importing '.concat(status['num_blocked']));
this.downProgBar.updateText(
'Importing '.concat(status['num_blocked'])
);
} else if (status['state'] == 'Idle') {
this.ProgressFset.hide();
this.checkDownload.getComponent(0).setDisabled(false);
@ -315,10 +330,12 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
this.lblFileSize.setText(fsize(status['file_size']));
this.lblDate.setText(fdate(status['file_date']));
this.lblType.setText(status['file_type']);
this.lblURL.setText(status['file_url'].substr(0,40).concat('...'));
this.lblURL.setText(
status['file_url'].substr(0, 40).concat('...')
);
}
},
scope: this
scope: this,
});
},
@ -329,7 +346,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
forceDown: function() {
this.onApply();
deluge.client.blocklist.check_import(force = true);
deluge.client.blocklist.check_import((force = true));
},
updateConfig: function() {
@ -348,7 +365,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
this.WhitelistFset.getComponent(0).loadData(data);
},
scope: this
scope: this,
});
deluge.client.blocklist.get_status({
@ -356,9 +373,11 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
this.lblFileSize.setText(fsize(status['file_size']));
this.lblDate.setText(fdate(status['file_date']));
this.lblType.setText(status['file_type']);
this.lblURL.setText(status['file_url'].substr(0,40).concat('...'));
this.lblURL.setText(
status['file_url'].substr(0, 40).concat('...')
);
},
scope: this
scope: this,
});
},
@ -366,7 +385,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
var store = this.WhitelistFset.getComponent(0).getStore();
var IP = store.recordType;
var i = new IP({
ip: ''
ip: '',
});
this.WhitelistFset.getComponent(0).stopEditing();
store.insert(0, i);
@ -374,12 +393,13 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
},
deleteIP: function() {
var selections = this.WhitelistFset.getComponent(0).getSelectionModel().getSelections();
var selections = this.WhitelistFset.getComponent(0)
.getSelectionModel()
.getSelections();
var store = this.WhitelistFset.getComponent(0).getStore();
this.WhitelistFset.getComponent(0).stopEditing();
for (var i = 0; i < selections.length; i++)
store.remove(selections[i]);
for (var i = 0; i < selections.length; i++) store.remove(selections[i]);
store.commitChanges();
},
@ -389,11 +409,10 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
deluge.preferences.un('show', this.updateConfig, this);
Deluge.ux.preferences.BlocklistPage.superclass.onDestroy.call(this);
}
},
});
Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
name: 'Blocklist',
onDisable: function() {
@ -401,8 +420,10 @@ Deluge.plugins.BlocklistPlugin = Ext.extend(Deluge.Plugin, {
},
onEnable: function() {
this.prefsPage = deluge.preferences.addPage(new Deluge.ux.preferences.BlocklistPage());
}
this.prefsPage = deluge.preferences.addPage(
new Deluge.ux.preferences.BlocklistPage()
);
},
});
Deluge.registerPlugin('Blocklist', Deluge.plugins.BlocklistPlugin);

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,36 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="window1">
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="GtkAdjustment" id="adjustment1">
<property name="lower">1</property>
<property name="upper">100</property>
<property name="value">1</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkWindow" id="window1">
<property name="can_focus">False</property>
<child>
<widget class="GtkVBox" id="blocklist_prefs_box">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="blocklist_prefs_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="orientation">vertical</property>
<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="GtkAlignment" id="alignment1">
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkHBox" id="hbox2">
<object class="GtkBox" id="hbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<widget class="GtkLabel" id="label3">
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">URL:</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -38,38 +49,33 @@
</packing>
</child>
<child>
<widget class="GtkEntry" id="entry_url">
<object class="GtkEntry" id="entry_url">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</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="ypad">5</property>
<property name="label" translatable="yes">&lt;b&gt;General&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">False</property>
<property name="fill">False</property>
@ -77,72 +83,71 @@
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame2">
<object class="GtkFrame" id="frame2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment2">
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkVBox" id="vbox1">
<object class="GtkBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkTable" id="table1">
<object class="GtkTable" id="table1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_columns">3</property>
<property name="column_spacing">5</property>
<property name="row_spacing">5</property>
<child>
<widget class="GtkLabel" id="label8">
<object class="GtkLabel" id="label8">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Days</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
<widget class="GtkSpinButton" id="spin_check_days">
<object class="GtkSpinButton" id="spin_check_days">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property>
<property name="adjustment">1 1 100 1 10 0</property>
</widget>
<property name="adjustment">adjustment1</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Check for new list every:</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -150,37 +155,33 @@
</packing>
</child>
<child>
<widget class="GtkCheckButton" id="chk_import_on_start">
<object class="GtkCheckButton" id="chk_import_on_start">
<property name="label" translatable="yes">Import blocklist on startup</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_action_appearance">False</property>
<property name="draw_indicator">True</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
</child>
<child>
<widget class="GtkLabel" id="label10">
<child type="label">
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ypad">5</property>
<property name="label" translatable="yes">&lt;b&gt;Settings&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">False</property>
<property name="fill">False</property>
@ -188,45 +189,45 @@
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame3">
<object class="GtkFrame" id="frame3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment3">
<object class="GtkAlignment" id="alignment3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xscale">0</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkHBox" id="hbox3">
<object class="GtkBox" id="hbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkVBox" id="vbox3">
<object class="GtkBox" id="vbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkButton" id="button_check_download">
<object class="GtkButton" id="button_check_download">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip" translatable="yes">Download the blocklist file if necessary and import the file.</property>
<property name="use_action_appearance">False</property>
<signal name="clicked" handler="on_button_check_download_clicked" />
<property name="tooltip_text" translatable="yes">Download the blocklist file if necessary and import the file.</property>
<signal name="clicked" handler="on_button_check_download_clicked" swapped="no"/>
<child>
<widget class="GtkHBox" id="hbox4">
<object class="GtkBox" id="hbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<widget class="GtkImage" id="image_download">
<object class="GtkImage" id="image_download">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-missing-image</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -234,20 +235,20 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label12">
<object class="GtkLabel" id="label12">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Check Download and Import</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -255,24 +256,23 @@
</packing>
</child>
<child>
<widget class="GtkButton" id="button_force_download">
<object class="GtkButton" id="button_force_download">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip" translatable="yes">Download a new blocklist file and import it.</property>
<property name="use_action_appearance">False</property>
<signal name="clicked" handler="on_button_force_download_clicked" />
<property name="tooltip_text" translatable="yes">Download a new blocklist file and import it.</property>
<signal name="clicked" handler="on_button_force_download_clicked" swapped="no"/>
<child>
<widget class="GtkHBox" id="hbox5">
<object class="GtkBox" id="hbox5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<widget class="GtkImage" id="image_import">
<object class="GtkImage" id="image_import">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-missing-image</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -280,27 +280,27 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label7">
<object class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Force Download and Import</property>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
@ -308,36 +308,33 @@
</packing>
</child>
<child>
<widget class="GtkImage" id="image_up_to_date">
<object class="GtkImage" id="image_up_to_date">
<property name="can_focus">False</property>
<property name="tooltip" translatable="yes">Blocklist is up to date</property>
<property name="tooltip_text" translatable="yes">Blocklist is up to date</property>
<property name="yalign">0.15000000596046448</property>
<property name="xpad">2</property>
<property name="stock">gtk-yes</property>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
</child>
<child>
<widget class="GtkLabel" id="label11">
<child type="label">
<object class="GtkLabel" id="label11">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ypad">5</property>
<property name="label" translatable="yes">&lt;b&gt;Options&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">False</property>
<property name="fill">False</property>
@ -345,26 +342,27 @@
</packing>
</child>
<child>
<widget class="GtkFrame" id="frame4">
<object class="GtkFrame" id="frame4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment4">
<object class="GtkAlignment" id="alignment4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">5</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkVBox" id="vbox4">
<object class="GtkBox" id="vbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkProgressBar" id="progressbar">
<object class="GtkProgressBar" id="progressbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
@ -372,18 +370,18 @@
</packing>
</child>
<child>
<widget class="GtkTable" id="table_info">
<object class="GtkTable" id="table_info">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">4</property>
<property name="n_columns">2</property>
<property name="column_spacing">5</property>
<child>
<widget class="GtkLabel" id="label_url">
<object class="GtkLabel" id="label_url">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</widget>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
@ -392,11 +390,11 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label_type">
<object class="GtkLabel" id="label_type">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</widget>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
@ -405,11 +403,11 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label_modified">
<object class="GtkLabel" id="label_modified">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</widget>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
@ -418,23 +416,23 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label_filesize">
<object class="GtkLabel" id="label_filesize">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</widget>
</object>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label17">
<object class="GtkLabel" id="label17">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">URL:</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
@ -442,12 +440,12 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label16">
<object class="GtkLabel" id="label16">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Type:</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
@ -455,12 +453,12 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label15">
<object class="GtkLabel" id="label15">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Date:</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
@ -468,39 +466,36 @@
</packing>
</child>
<child>
<widget class="GtkLabel" id="label14">
<object class="GtkLabel" id="label14">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">File Size:</property>
</widget>
<property name="xalign">0</property>
</object>
<packing>
<property name="x_options">GTK_FILL</property>
</packing>
</child>
</widget>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</object>
</child>
<child>
<widget class="GtkLabel" id="label13">
<child type="label">
<object class="GtkLabel" id="label13">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Info&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">False</property>
<property name="fill">False</property>
@ -508,35 +503,36 @@
</packing>
</child>
<child>
<widget class="GtkFrame" id="whitelist_frame">
<object class="GtkFrame" id="whitelist_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">none</property>
<child>
<widget class="GtkAlignment" id="alignment5">
<object class="GtkAlignment" id="alignment5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="left_padding">12</property>
<child>
<widget class="GtkHBox" id="hbox1">
<object class="GtkBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow1">
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<widget class="GtkTreeView" id="whitelist_treeview">
<object class="GtkTreeView" id="whitelist_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="headers_clickable">False</property>
</widget>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</widget>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
@ -544,21 +540,20 @@
</packing>
</child>
<child>
<widget class="GtkVButtonBox" id="vbuttonbox1">
<object class="GtkVButtonBox" id="vbuttonbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="layout_style">start</property>
<child>
<widget class="GtkButton" id="whitelist_add">
<object class="GtkButton" id="whitelist_add">
<property name="label">gtk-add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_whitelist_add_clicked" />
</widget>
<signal name="clicked" handler="on_whitelist_add_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
@ -566,51 +561,47 @@
</packing>
</child>
<child>
<widget class="GtkButton" id="whitelist_delete">
<object class="GtkButton" id="whitelist_delete">
<property name="label">gtk-delete</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_whitelist_remove_clicked" />
</widget>
<signal name="clicked" handler="on_whitelist_remove_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</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>
</child>
<child>
<widget class="GtkLabel" id="label2">
<child type="label">
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">&lt;b&gt;Whitelist&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">4</property>
</packing>
</child>
</widget>
</object>
</child>
</widget>
</glade-interface>
</object>
</interface>

View File

@ -17,25 +17,31 @@ import zipfile
def Zipped(reader): # NOQA: N802
"""Blocklist reader for zipped blocklists"""
def open(self):
def _open(self):
z = zipfile.ZipFile(self.file)
f = z.open(z.namelist()[0])
return f
reader.open = open
reader.open = _open
return reader
def GZipped(reader): # NOQA: N802
"""Blocklist reader for gzipped blocklists"""
def open(self):
def _open(self):
return gzip.open(self.file)
reader.open = open
reader.open = _open
return reader
def BZipped2(reader): # NOQA: N802
"""Blocklist reader for bzipped2 blocklists"""
def open(self):
def _open(self):
return bz2.BZ2File(self.file)
reader.open = open
reader.open = _open
return reader

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