1
0
Fork 0

Merging upstream version 2.1.1 (Closes: #1026291).

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-17 07:39:33 +01:00
parent d90f010c88
commit 111de75aff
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
522 changed files with 304108 additions and 161556 deletions

17
AUTHORS
View file

@ -39,14 +39,9 @@ Images Authors:
* files: deluge/ui/data/pixmaps/*.svg, *.png
deluge/ui/web/icons/active.png, alert.png, all.png, checking.png, dht.png,
downloading.png, inactive.png, queued.png, seeding.png, traffic.png
exceptions: deluge/ui/data/pixmaps/deluge.svg and derivatives
copyright: Andrew Resch
license: GPLv3
* files: deluge/ui/data/pixmaps/deluge.svg and derivatives
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
deluge/ui/web/images/deluge*.png
copyright: Andrew Wedderburn
deluge/ui/web/icons/apple-pre-*.png, deluge*.png
copyright: Calum Lind
license: GPLv3
* files: deluge/plugins/blocklist/blocklist/data/*.png
@ -55,11 +50,9 @@ Images Authors:
license: GPLv2
url: http://ftp.acc.umu.se/pub/GNOME/sources/gnome-icon-theme
* files: deluge/ui/data/pixmaps/magnet.png
copyright: Woothemes
license: Freeware
icon pack: WP Woothemes Ultimate
url: http://www.woothemes.com/
* files: deluge/ui/data/pixmaps/magnet*.svg, *.png
copyright: Matias Wilkman
license:
* files: deluge/ui/data/pixmaps/flags/*.png
copyright: Mark James <mjames@gmail.com>

View file

@ -1,5 +1,164 @@
# Changelog
## 2.1.1 (2022-07-10)
### Core
- Fix missing trackers added via magnet
- Fix handling magnets with tracker tiers
## 2.1.0 (2022-06-28)
### Breaking changes
- Python 2 support removed (Python >= 3.6)
- libtorrent minimum requirement increased (>= 1.2).
### Core
- Add support for SVG tracker icons.
- Fix tracker icon error handling.
- Fix cleaning-up tracker icon temp files.
- Fix Plugin manager to handle new metadata 2.1.
- Hide passwords in config logs.
- Fix cleaning-up temp files in add_torrent_url.
- Fix KeyError in sessionproxy after torrent delete.
- Remove libtorrent deprecated functions.
- Fix file_completed_alert handling.
- Add plugin keys to get_torrents_status.
- Add support for pygeoip dependency.
- Fix crash logging to Windows protected folder.
- Add is_interface and is_interface_name to validate network interfaces.
- Fix is_url and is_infohash error with None value.
- Fix load_libintl error.
- Add support for IPv6 in host lists.
- Add systemd user services.
- Fix refresh and expire the torrent status cache.
- Fix crash when logging errors initializing gettext.
### Web UI
- Fix ETA column sorting in correct order (#3413).
- Fix defining foreground and background colors.
- Accept charset in content-type for json messages.
- Fix 'Complete Seen' and 'Completed' sorting.
- Fix encoding HTML entities for torrent attributes to prevent XSS.
### Gtk UI
- Fix download location textbox width.
- Fix obscured port number in Connection Manager.
- Increase connection manager default height.
- Fix bug with setting move completed in Options tab.
- Fix adding daemon accounts.
- Add workaround for crash on Windows with ico or gif icons.
- Hide account password length in log.
- Added a torrent menu option for magnet copy.
- Fix unable to prefetch magnet in thinclient mode.
- Use GtkSpinner when testing open port.
- Update About Dialog year.
- Fix Edit Torrents dialogs close issues.
- Fix ETA being copied to neighboring empty cells.
- Disable GTK CSD by default on Windows.
### Console UI
- Fix curses.init_pair raise ValueError on Py3.10.
- Swap j and k key's behavior to fit vim mode.
- Fix torrent details status error.
- Fix incorrect test for when a host is online.
- Add the torrent label to info command.
### AutoAdd
- Fix handling torrent decode errors.
- Fix error dialog not being shown on error.
### Blocklist
- Add frequency unit to interval label.
### Notifications
- Fix UnicodeEncodeError upon non-ascii torrent name.
## 2.0.5 (2021-12-15)
### WebUI
- Fix js minifying error resulting in WebUI blank screen.
- Silence erronous missing translations warning.
## 2.0.4 (2021-12-12)
### Packaging
- Fix python optional setup.py requirements
### Gtk UI
- Add detection of torrent URL on GTK UI focus
- Fix piecesbar crashing when enabled
- Remove num_blocks_cache_hits in stats
- Fix unhandled error with empty clipboard
- Add torrentdetails tabs position menu (#3441)
- Hide pygame community banner in console
- Fix cmp function for None types (#3309)
- Fix loading config with double-quotes in string
- Fix Status tab download speed and uploaded
### Web UI
- Handle torrent add failures
- Add menu option to copy magnet URI
- Fix md5sums in torrent files breaking file listing (#3388)
- Add country flag alt/title for accessibility
### Console UI
- Fix allowing use of windows-curses on Windows
- Fix hostlist status lookup errors
- Fix AttributeError setting config values
- Fix setting 'Skip' priority
### Core
- Add workaround libtorrent 2.0 file_progress error
- Fix allow enabling any plugin Python version
- Export torrent get_magnet_uri method
- Fix loading magnet with resume_data and no metadata (#3478)
- Fix httpdownloader reencoding torrent file downloads (#3440)
- Fix lt listen_interfaces not comma-separated (#3337)
- Fix unable to remove magnet with delete_copies enabled (#3325)
- Fix Python 3.8 compatibility
- Fix loading config with double-quotes in string
- Fix pickle loading non-ascii state error (#3298)
- Fix creation of pidfile via command option
- Fix for peer.client UnicodeDecodeError
- Fix show_file unhandled dbus error
### Documentation
- Add How-to guides about services.
### Stats plugin
- Fix constant session status key warnings
- Fix cairo error
### Notifications plugin
- Fix email KeyError with status name
- Fix unhandled TypeErrors on Python 3
### Autoadd plugin
- Fix magnet missing applied labels
### Execute plugin
- Fix failing to run on Windows (#3439)
## 2.0.3 (2019-06-12)
### Gtk UI
@ -31,13 +190,13 @@
### Core
- Fix Python 2 compatiblity issue with SimpleNamespace.
- Fix Python 2 compatibility issue with SimpleNamespace.
## 2.0.1 (2019-06-07)
### Packaging
- Fix setup.py build error without git installed.
- Fix `setup.py` build error without git installed.
## 2.0.0 (2019-06-06)
@ -55,16 +214,16 @@
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.
and torrents loaded from state. This will break backwards compatibility.
- Pass a copy of an event instead of passing the event arguments to the
event handlers. This will break backwards compatability.
event handlers. This will break backwards compatibility.
- 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.
which should 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.
@ -77,13 +236,13 @@
- 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.
- 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
- Up: `Ctrl+Alt+Up`
- Down: `Ctrl+Alt+Down`
- Top: `Ctrl+Alt+Shift+Up`
- Bottom: `Ctrl+Alt+Shift+Down`
### Web UI
@ -93,7 +252,7 @@
### Blocklist Plugin
- Implemented whitelist support to both core and GTK UI.
- Implemented ip filter cleaning before each update. Restarting the deluge
- 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

View file

@ -7,13 +7,13 @@ All modules will require the [common](#common) section dependencies.
## Prerequisite
- [Python] _>= 3.5_
- [Python] _>= 3.6_
## Build
- [setuptools]
- [intltool] - Optional: Desktop file translation for \*nix.
- [closure-compiler] - Minify javascript (alternative is [slimit])
- [closure-compiler] - Minify javascript (alternative is [rjsmin])
## Common
@ -23,26 +23,26 @@ All modules will require the [common](#common) section dependencies.
- [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.
- [ifaddr] - Optional: Verify network interfaces.
#### Linux and BSD
### Linux and BSD
- [distro] - Optional: OS platform information.
#### Windows OS
### Windows OS
- [pywin32]
- [certifi]
## Core (deluged daemon)
- [libtorrent] _>= 1.1.1_
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_)
- [libtorrent] _>= 1.2.0_
- [GeoIP] or [pygeoip] - Optional: IP address country lookup. (_Debian: `python-geoip`_)
## GTK UI
@ -52,7 +52,7 @@ All modules will require the [common](#common) section dependencies.
- [librsvg] _>= 2_
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
#### MacOS
### MacOS
- [GtkOSXApplication]
@ -71,7 +71,7 @@ All modules will require the [common](#common) section dependencies.
[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/
[rjsmin]: https://pypi.org/project/rjsmin/
[openssl]: https://www.openssl.org/
[pyopenssl]: https://pyopenssl.org
[twisted]: https://twistedmatrix.com
@ -81,14 +81,12 @@ All modules will require the [common](#common) section dependencies.
[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/
@ -99,3 +97,4 @@ All modules will require the [common](#common) section dependencies.
[libnotify]: https://developer.gnome.org/libnotify/
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
[ifaddr]: https://pypi.org/project/ifaddr/

View file

@ -23,7 +23,7 @@ recursive-exclude deluge/tests *.pyc
graft deluge/ui/data
recursive-exclude deluge/ui/data *.desktop *.xml
graft deluge/ui/gtkui/glade
graft deluge/ui/gtk3/glade
include deluge/ui/web/index.html
include deluge/ui/web/css/*.css

144
PKG-INFO
View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: deluge
Version: 2.0.3
Version: 2.1.1
Summary: BitTorrent Client
Home-page: https://deluge-torrent.org
Author: Deluge Team
@ -12,72 +12,7 @@ Project-URL: Sourcecode, http://git.deluge-torrent.org/deluge
Project-URL: Issues, https://dev.deluge-torrent.org/report/1
Project-URL: Discussion, https://forum.deluge-torrent.org
Project-URL: Documentation, https://deluge.readthedocs.io
Description: # 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
Keywords: torrent bittorrent p2p fileshare filesharing
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
@ -90,5 +25,80 @@ Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Topic :: Internet
Requires-Python: >=2.7
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: all
License-File: LICENSE
License-File: AUTHORS
# Deluge BitTorrent Client
[![build-status]][github-ci] [![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
Console-UI. It uses [libtorrent][lt] at its core to handle the BitTorrent
protocol.
## Install
From [PyPi](https://pypi.org/project/deluge):
pip install deluge
with all optional dependencies:
pip install deluge[all]
From source code:
pip install .
with all optional dependencies:
pip install .[all]
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 Libera.Chat #deluge](irc://irc.libera.chat/deluge)
- [Discord](https://discord.gg/nwaHSE6tqn)
[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://github.com/deluge-torrent/deluge/actions/workflows/ci.yml/badge.svg?branch=develop "CI"
[github-ci]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml
[docs-status]: https://readthedocs.org/projects/deluge/badge/?version=latest
[rtd-deluge]: https://deluge.readthedocs.io/en/latest/?badge=latest "Documentation Status"
[lt]: https://libtorrent.org

View file

@ -1,10 +1,10 @@
# Deluge BitTorrent Client
[![build-status]][travis-deluge] [![docs-status]][rtd-deluge]
[![build-status]][github-ci] [![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
Console-UI. It uses [libtorrent][lt] at its core to handle the BitTorrent
protocol.
## Install
@ -13,10 +13,17 @@ From [PyPi](https://pypi.org/project/deluge):
pip install deluge
with all optional dependencies:
pip install deluge[all]
From source code:
python setup.py build
python setup.py install
pip install .
with all optional dependencies:
pip install .[all]
See [DEPENDS](DEPENDS.md) and [Installing/Source] for dependency details.
@ -51,13 +58,14 @@ See the [Thinclient guide] to connect to the daemon from another computer.
- [Homepage](https://deluge-torrent.org)
- [User guide][user guide]
- [Forum](https://forum.deluge-torrent.org)
- [IRC Freenode #deluge](irc://irc.freenode.net/deluge)
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
- [Discord](https://discord.gg/nwaHSE6tqn)
[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"
[build-status]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml/badge.svg?branch=develop "CI"
[github-ci]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml
[docs-status]: https://readthedocs.org/projects/deluge/badge/?version=latest
[rtd-deluge]: https://deluge.readthedocs.io/en/latest/?badge=latest "Documentation Status"
[lt]: https://libtorrent.org

View file

@ -1 +1 @@
2.0.3
2.1.1

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: deluge
Version: 2.0.3
Version: 2.1.1
Summary: BitTorrent Client
Home-page: https://deluge-torrent.org
Author: Deluge Team
@ -12,72 +12,7 @@ Project-URL: Sourcecode, http://git.deluge-torrent.org/deluge
Project-URL: Issues, https://dev.deluge-torrent.org/report/1
Project-URL: Discussion, https://forum.deluge-torrent.org
Project-URL: Documentation, https://deluge.readthedocs.io
Description: # 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
Keywords: torrent bittorrent p2p fileshare filesharing
Platform: UNKNOWN
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
@ -90,5 +25,80 @@ Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Topic :: Internet
Requires-Python: >=2.7
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: all
License-File: LICENSE
License-File: AUTHORS
# Deluge BitTorrent Client
[![build-status]][github-ci] [![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
Console-UI. It uses [libtorrent][lt] at its core to handle the BitTorrent
protocol.
## Install
From [PyPi](https://pypi.org/project/deluge):
pip install deluge
with all optional dependencies:
pip install deluge[all]
From source code:
pip install .
with all optional dependencies:
pip install .[all]
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 Libera.Chat #deluge](irc://irc.libera.chat/deluge)
- [Discord](https://discord.gg/nwaHSE6tqn)
[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://github.com/deluge-torrent/deluge/actions/workflows/ci.yml/badge.svg?branch=develop "CI"
[github-ci]: https://github.com/deluge-torrent/deluge/actions/workflows/ci.yml
[docs-status]: https://readthedocs.org/projects/deluge/badge/?version=latest
[rtd-deluge]: https://deluge.readthedocs.io/en/latest/?badge=latest "Documentation Status"
[lt]: https://libtorrent.org

View file

@ -8,6 +8,7 @@ RELEASE-VERSION
gen_web_gettext.py
minify_web_js.py
msgfmt.py
pyproject.toml
setup.cfg
setup.py
version.py
@ -19,6 +20,7 @@ deluge/common.py
deluge/component.py
deluge/config.py
deluge/configmanager.py
deluge/conftest.py
deluge/crypto_utils.py
deluge/decorators.py
deluge/error.py
@ -50,6 +52,7 @@ deluge/core/rpcserver.py
deluge/core/torrent.py
deluge/core/torrentmanager.py
deluge/i18n/__init__.py
deluge/i18n/af.po
deluge/i18n/ar.po
deluge/i18n/ast.po
deluge/i18n/be.po
@ -71,8 +74,10 @@ deluge/i18n/et.po
deluge/i18n/eu.po
deluge/i18n/fa.po
deluge/i18n/fi.po
deluge/i18n/fo.po
deluge/i18n/fr.po
deluge/i18n/fy.po
deluge/i18n/ga.po
deluge/i18n/gl.po
deluge/i18n/he.po
deluge/i18n/hi.po
@ -85,18 +90,25 @@ deluge/i18n/iu.po
deluge/i18n/ja.po
deluge/i18n/ka.po
deluge/i18n/kk.po
deluge/i18n/km.po
deluge/i18n/kn.po
deluge/i18n/ko.po
deluge/i18n/ku.po
deluge/i18n/ky.po
deluge/i18n/la.po
deluge/i18n/languages.py
deluge/i18n/lb.po
deluge/i18n/lt.po
deluge/i18n/lv.po
deluge/i18n/mk.po
deluge/i18n/ml.po
deluge/i18n/ms.po
deluge/i18n/nap.po
deluge/i18n/nb.po
deluge/i18n/nds.po
deluge/i18n/nl.po
deluge/i18n/nn.po
deluge/i18n/oc.po
deluge/i18n/pl.po
deluge/i18n/pms.po
deluge/i18n/pt.po
@ -109,11 +121,13 @@ deluge/i18n/sl.po
deluge/i18n/sr.po
deluge/i18n/sv.po
deluge/i18n/ta.po
deluge/i18n/te.po
deluge/i18n/th.po
deluge/i18n/tl.po
deluge/i18n/tlh.po
deluge/i18n/tr.po
deluge/i18n/uk.po
deluge/i18n/ur.po
deluge/i18n/util.py
deluge/i18n/vi.po
deluge/i18n/zh_CN.po
@ -149,7 +163,6 @@ deluge/plugins/Blocklist/deluge_blocklist/data/blocklist16.png
deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_download24.png
deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_import24.png
deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui
deluge/plugins/Blocklist/deluge_blocklist/data/blocklist_pref.ui~
deluge/plugins/Execute/setup.py
deluge/plugins/Execute/deluge_execute/__init__.py
deluge/plugins/Execute/deluge_execute/common.py
@ -158,7 +171,6 @@ deluge/plugins/Execute/deluge_execute/gtkui.py
deluge/plugins/Execute/deluge_execute/webui.py
deluge/plugins/Execute/deluge_execute/data/execute.js
deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui
deluge/plugins/Execute/deluge_execute/data/execute_prefs.ui~
deluge/plugins/Extractor/setup.py
deluge/plugins/Extractor/deluge_extractor/__init__.py
deluge/plugins/Extractor/deluge_extractor/common.py
@ -167,7 +179,6 @@ deluge/plugins/Extractor/deluge_extractor/gtkui.py
deluge/plugins/Extractor/deluge_extractor/webui.py
deluge/plugins/Extractor/deluge_extractor/data/extractor.js
deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui
deluge/plugins/Extractor/deluge_extractor/data/extractor_prefs.ui~
deluge/plugins/Label/TODO
deluge/plugins/Label/setup.py
deluge/plugins/Label/deluge_label/__init__.py
@ -191,7 +202,6 @@ deluge/plugins/Notifications/deluge_notifications/gtkui.py
deluge/plugins/Notifications/deluge_notifications/test.py
deluge/plugins/Notifications/deluge_notifications/webui.py
deluge/plugins/Notifications/deluge_notifications/data/config.ui
deluge/plugins/Notifications/deluge_notifications/data/config.ui~
deluge/plugins/Notifications/deluge_notifications/data/notifications.js
deluge/plugins/Scheduler/setup.py
deluge/plugins/Scheduler/deluge_scheduler/__init__.py
@ -211,7 +221,6 @@ deluge/plugins/Stats/deluge_stats/graph.py
deluge/plugins/Stats/deluge_stats/gtkui.py
deluge/plugins/Stats/deluge_stats/webui.py
deluge/plugins/Stats/deluge_stats/data/config.ui
deluge/plugins/Stats/deluge_stats/data/config.ui~
deluge/plugins/Stats/deluge_stats/data/stats.js
deluge/plugins/Stats/deluge_stats/data/tabs.ui
deluge/plugins/Stats/deluge_stats/template/graph.html
@ -231,11 +240,9 @@ deluge/plugins/WebUi/deluge_webui/common.py
deluge/plugins/WebUi/deluge_webui/core.py
deluge/plugins/WebUi/deluge_webui/gtkui.py
deluge/plugins/WebUi/deluge_webui/data/config.ui
deluge/plugins/WebUi/deluge_webui/data/config.ui~
deluge/plugins/WebUi/deluge_webui/tests/__init__.py
deluge/plugins/WebUi/deluge_webui/tests/test_plugin_webui.py
deluge/tests/__init__.py
deluge/tests/basetest.py
deluge/tests/common.py
deluge/tests/common_web.py
deluge/tests/daemon_base.py
@ -254,6 +261,7 @@ deluge/tests/test_httpdownloader.py
deluge/tests/test_json_api.py
deluge/tests/test_log.py
deluge/tests/test_maketorrent.py
deluge/tests/test_maybe_coroutine.py
deluge/tests/test_metafile.py
deluge/tests/test_plugin_metadata.py
deluge/tests/test_rpcserver.py
@ -267,6 +275,7 @@ deluge/tests/test_transfer.py
deluge/tests/test_ui_common.py
deluge/tests/test_ui_console.py
deluge/tests/test_ui_entry.py
deluge/tests/test_ui_gtk3.py
deluge/tests/test_web_api.py
deluge/tests/test_web_auth.py
deluge/tests/test_webserver.py
@ -274,13 +283,30 @@ deluge/tests/data/deluge.png
deluge/tests/data/dir_with_6_files.torrent
deluge/tests/data/filehash_field.torrent
deluge/tests/data/google.ico
deluge/tests/data/seo.ico
deluge/tests/data/md5sum.torrent
deluge/tests/data/seo.svg
deluge/tests/data/test.torrent
deluge/tests/data/test_torrent.file.torrent
deluge/tests/data/testssl.sh
deluge/tests/data/ubuntu-9.04-desktop-i386.iso.torrent
deluge/tests/data/unicode_file.torrent
deluge/tests/data/unicode_filenames.torrent
deluge/tests/twisted/plugins/delugereporter.py
deluge/tests/data/utf8_filename_torrents.state
deluge/tests/data/etc/Apple.pem
deluge/tests/data/etc/Java.pem
deluge/tests/data/etc/Linux.pem
deluge/tests/data/etc/Microsoft.pem
deluge/tests/data/etc/Mozilla.pem
deluge/tests/data/etc/README.md
deluge/tests/data/etc/ca_hashes.txt
deluge/tests/data/etc/cipher-mapping.txt
deluge/tests/data/etc/client-simulation.txt
deluge/tests/data/etc/client-simulation.wiresharked.md
deluge/tests/data/etc/client-simulation.wiresharked.txt
deluge/tests/data/etc/common-primes.txt
deluge/tests/data/etc/curves.txt
deluge/tests/data/etc/openssl.cnf
deluge/tests/data/etc/tls_data.txt
deluge/ui/__init__.py
deluge/ui/client.py
deluge/ui/common.py
@ -350,7 +376,6 @@ deluge/ui/console/widgets/popup.py
deluge/ui/console/widgets/sidebar.py
deluge/ui/console/widgets/statusbars.py
deluge/ui/console/widgets/window.py
deluge/ui/data/__pycache__/__init__.cpython-37.pyc
deluge/ui/data/icons/hicolor/128x128/apps/deluge.png
deluge/ui/data/icons/hicolor/16x16/apps/deluge-panel.png
deluge/ui/data/icons/hicolor/16x16/apps/deluge.png
@ -388,7 +413,12 @@ deluge/ui/data/pixmaps/downloading16.png
deluge/ui/data/pixmaps/inactive.svg
deluge/ui/data/pixmaps/inactive16.png
deluge/ui/data/pixmaps/loading.gif
deluge/ui/data/pixmaps/magnet.png
deluge/ui/data/pixmaps/magnet.svg
deluge/ui/data/pixmaps/magnet16.png
deluge/ui/data/pixmaps/magnet_add.svg
deluge/ui/data/pixmaps/magnet_add16.png
deluge/ui/data/pixmaps/magnet_copy.svg
deluge/ui/data/pixmaps/magnet_copy16.png
deluge/ui/data/pixmaps/queued.svg
deluge/ui/data/pixmaps/queued16.png
deluge/ui/data/pixmaps/seeding.svg
@ -397,7 +427,6 @@ deluge/ui/data/pixmaps/tracker_all16.png
deluge/ui/data/pixmaps/tracker_warning16.png
deluge/ui/data/pixmaps/traffic.svg
deluge/ui/data/pixmaps/traffic16.png
deluge/ui/data/pixmaps/__pycache__/__init__.cpython-37.pyc
deluge/ui/data/pixmaps/flags/ad.png
deluge/ui/data/pixmaps/flags/ae.png
deluge/ui/data/pixmaps/flags/af.png
@ -702,6 +731,7 @@ deluge/ui/gtk3/glade/main_window.new_release.ui
deluge/ui/gtk3/glade/main_window.tabs.menu_file.ui
deluge/ui/gtk3/glade/main_window.tabs.menu_peer.ui
deluge/ui/gtk3/glade/main_window.tabs.ui
deluge/ui/gtk3/glade/main_window.tabs.ui~
deluge/ui/gtk3/glade/main_window.ui
deluge/ui/gtk3/glade/move_storage_dialog.ui
deluge/ui/gtk3/glade/other_dialog.ui
@ -728,7 +758,6 @@ deluge/ui/web/css/ext-extensions.css
deluge/ui/web/icons/active.png
deluge/ui/web/icons/add.png
deluge/ui/web/icons/add_file.png
deluge/ui/web/icons/add_magnet.png
deluge/ui/web/icons/add_url.png
deluge/ui/web/icons/alert.png
deluge/ui/web/icons/all.png
@ -762,6 +791,9 @@ deluge/ui/web/icons/install_plugin.png
deluge/ui/web/icons/login.png
deluge/ui/web/icons/logout.png
deluge/ui/web/icons/low.png
deluge/ui/web/icons/magnet.png
deluge/ui/web/icons/magnet_add.png
deluge/ui/web/icons/magnet_copy.png
deluge/ui/web/icons/move.png
deluge/ui/web/icons/no_download.png
deluge/ui/web/icons/normal.png
@ -784,6 +816,7 @@ deluge/ui/web/images/s.gif
deluge/ui/web/images/spinner-split.gif
deluge/ui/web/images/spinner.gif
deluge/ui/web/js/deluge-all-debug.js
deluge/ui/web/js/deluge-all.js
deluge/ui/web/js/gettext.js
deluge/ui/web/js/deluge-all/.order
deluge/ui/web/js/deluge-all/AboutWindow.js
@ -791,6 +824,7 @@ deluge/ui/web/js/deluge-all/AddConnectionWindow.js
deluge/ui/web/js/deluge-all/AddTrackerWindow.js
deluge/ui/web/js/deluge-all/Client.js
deluge/ui/web/js/deluge-all/ConnectionManager.js
deluge/ui/web/js/deluge-all/CopyMagnetWindow.js
deluge/ui/web/js/deluge-all/Deluge.js
deluge/ui/web/js/deluge-all/EditConnectionWindow.js
deluge/ui/web/js/deluge-all/EditTrackerWindow.js
@ -851,6 +885,7 @@ deluge/ui/web/js/extjs/ext-all.js
deluge/ui/web/js/extjs/ext-base-debug.js
deluge/ui/web/js/extjs/ext-base.js
deluge/ui/web/js/extjs/ext-extensions-debug.js
deluge/ui/web/js/extjs/ext-extensions.js
deluge/ui/web/js/extjs/ext-extensions/JSLoader.js
deluge/ui/web/js/extjs/ext-extensions/Spinner.js
deluge/ui/web/js/extjs/ext-extensions/StatusBar.js
@ -1390,4 +1425,6 @@ docs/man/deluge.1
docs/man/deluged.1
packaging/systemd/deluge-web.service
packaging/systemd/deluged.service
packaging/systemd/user.conf
packaging/systemd/user.conf
packaging/systemd/user/deluge-web.service
packaging/systemd/user/deluged.service

View file

@ -1,7 +1,5 @@
[console_scripts]
deluge-console = deluge.ui.console:start
deluge-web = deluge.ui.web:start
deluged = deluge.core.daemon_entry:start_daemon
[deluge.ui]
console = deluge.ui.console:Console
@ -11,4 +9,5 @@ web = deluge.ui.web:Web
[gui_scripts]
deluge = deluge.ui.ui_entry:start_ui
deluge-gtk = deluge.ui.gtk3:start
deluge-web = deluge.ui.web:start
deluged = deluge.core.daemon_entry:start_daemon

View file

@ -3,16 +3,16 @@ pyasn1
rencode
pyopenssl
pyxdg
pillow
mako
chardet
six
setproctitle
setuptools
zope.interface
[:sys_platform == "win32"]
pywin32
certifi
[:sys_platform == "win32" and python_version == "2"]
py2-ipaddress
[all]
setproctitle
pillow
chardet
ifaddr

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -15,19 +14,22 @@
>>> from deluge._libtorrent import lt
"""
from __future__ import unicode_literals
from deluge.common import VersionSplit, get_version
from deluge.error import LibtorrentImportError
try:
import deluge.libtorrent as lt
except ImportError:
import libtorrent as lt
try:
import libtorrent as lt
except ImportError as ex:
raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
REQUIRED_VERSION = '1.1.2.0'
REQUIRED_VERSION = '1.2.0.0'
LT_VERSION = lt.__version__
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
raise ImportError(
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION)
raise LibtorrentImportError(
f'Deluge {get_version()} requires libtorrent >= {REQUIRED_VERSION}'
)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -7,8 +6,6 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import argparse
import logging
import os
@ -95,7 +92,7 @@ def _get_version_detail():
except ImportError:
pass
version_str += 'Python: %s\n' % platform.python_version()
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version())
version_str += f'OS: {platform.system()} {common.get_os_version()}\n'
return version_str
@ -109,8 +106,8 @@ def _split_lines(self, text, width):
line instead. This way list formatting is not mangled by textwrap.wrap.
"""
wrapped_lines = []
for l in text.splitlines():
wrapped_lines.extend(textwrap.wrap(l, width, subsequent_indent=' '))
for line in text.splitlines():
wrapped_lines.extend(textwrap.wrap(line, width, subsequent_indent=' '))
return wrapped_lines
def _format_action_invocation(self, action):
@ -122,7 +119,7 @@ def _format_action_invocation(self, action):
"""
if not action.option_strings:
metavar, = self._metavar_formatter(action, action.dest)(1)
(metavar,) = self._metavar_formatter(action, action.dest)(1)
return metavar
else:
parts = []
@ -137,7 +134,7 @@ def _format_action_invocation(self, action):
default = action.dest.upper()
args_string = self._format_args(action, default)
opt = ', '.join(action.option_strings)
parts.append('%s %s' % (opt, args_string))
parts.append(f'{opt} {args_string}')
return ', '.join(parts)
@ -165,7 +162,7 @@ def __init__(self, *args, **kwargs):
self.log_stream = kwargs['log_stream']
del kwargs['log_stream']
super(ArgParserBase, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.common_setup = False
self.process_arg_group = False
@ -202,7 +199,7 @@ def __init__(self, *args, **kwargs):
self.group.add_argument(
'-L',
'--loglevel',
choices=[l for k in deluge.log.levels for l in (k, k.upper())],
choices=[level for k in deluge.log.levels for level in (k, k.upper())],
help=_('Set the log level (none, error, warning, info, debug)'),
metavar='<level>',
)
@ -246,7 +243,7 @@ def parse_args(self, args=None):
argparse.Namespace: The parsed arguments.
"""
options = super(ArgParserBase, self).parse_args(args=args)
options = super().parse_args(args=args)
return self._handle_ui_options(options)
def parse_known_ui_args(self, args, withhold=None):
@ -262,9 +259,9 @@ def parse_known_ui_args(self, args, withhold=None):
"""
if withhold:
args = [a for a in args if a not in withhold]
options, remaining = super(ArgParserBase, self).parse_known_args(args=args)
options, remaining = super().parse_known_args(args=args)
options.remaining = remaining
# Hanlde common and process group options
# Handle common and process group options
return self._handle_ui_options(options)
def _handle_ui_options(self, options):
@ -325,22 +322,22 @@ def _handle_ui_options(self, options):
# Write pid file before chuid
if options.pidfile:
with open(options.pidfile, 'wb') as _file:
with open(options.pidfile, 'w') as _file:
_file.write('%d\n' % os.getpid())
if not common.windows_check():
if options.group:
if not options.group.isdigit():
import grp
options.group = grp.getgrnam(options.group)[2]
os.setgid(options.group)
if options.user:
if not options.user.isdigit():
import pwd
options.user = pwd.getpwnam(options.user)[2]
os.setuid(options.user)
if options.group:
if not options.group.isdigit():
import grp
options.group = grp.getgrnam(options.group)[2]
os.setuid(options.group)
return options

View file

@ -9,13 +9,7 @@
# License.
# Written by Petru Paler
# Updated by Calum Lind to support both Python 2 and Python 3.
from __future__ import unicode_literals
from sys import version_info
PY2 = version_info.major == 2
# Updated by Calum Lind to support Python 3.
class BTFailure(Exception):
@ -90,7 +84,7 @@ def bdecode(x):
return r
class Bencached(object):
class Bencached:
__slots__ = ['bencoded']
@ -146,10 +140,6 @@ def encode_dict(x, r):
encode_func[bool] = encode_bool
encode_func[str] = encode_string
encode_func[bytes] = encode_bytes
if PY2:
encode_func[long] = encode_int # noqa: F821
encode_func[str] = encode_bytes
encode_func[unicode] = encode_string # noqa: F821
def bencode(x):

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
#
@ -8,25 +7,25 @@
#
"""Common functions for various parts of Deluge to use."""
from __future__ import division, print_function, unicode_literals
import base64
import binascii
import functools
import glob
import locale
import logging
import numbers
import os
import platform
import re
import socket
import subprocess
import sys
import tarfile
import time
from contextlib import closing
from datetime import datetime
from io import BytesIO, open
from io import BytesIO
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
import pkg_resources
@ -38,14 +37,6 @@
except ImportError:
chardet = None
try:
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
except ImportError:
# PY2 fallback
from urlparse import urljoin # pylint: disable=ungrouped-imports
from urllib import pathname2url, unquote_plus # pylint: disable=ungrouped-imports
# Windows workaround for HTTPS requests requiring certificate authority bundle.
# see: https://twistedmatrix.com/trac/ticket/9209
if platform.system() in ('Windows', 'Microsoft'):
@ -53,6 +44,11 @@
os.environ['SSL_CERT_FILE'] = where()
try:
import ifaddr
except ImportError:
ifaddr = None
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
# gi makes dbus available on Window but don't import it as unused.
@ -81,7 +77,11 @@
# The output formatting for json.dump
JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False}
PY2 = sys.version_info.major == 2
DBUS_FM_ID = 'org.freedesktop.FileManager1'
DBUS_FM_PATH = '/org/freedesktop/FileManager1'
# Retained for plugin backward compatibility
PY2 = False
def get_version():
@ -108,10 +108,8 @@ def get_default_config_dir(filename=None):
def save_config_path(resource):
app_data_path = os.environ.get('APPDATA')
if not app_data_path:
try:
import winreg
except ImportError:
import _winreg as winreg # For Python 2.
import winreg
hkey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
@ -144,14 +142,14 @@ def get_default_download_dir():
try:
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
with open(user_dirs_path, 'r', encoding='utf8') as _file:
with open(user_dirs_path, 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('"')
)
break
except IOError:
except OSError:
pass
if not download_dir:
@ -175,8 +173,8 @@ def archive_files(arc_name, filepaths, message=None, rotate=10):
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
arc_comp = 'xz'
archive_dir = os.path.join(get_config_dir(), 'archive')
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
@ -272,7 +270,7 @@ def get_os_version():
os_version = list(platform.mac_ver())
os_version[1] = '' # versioninfo always empty.
elif distro:
os_version = distro.linux_distribution()
os_version = (distro.name(), distro.version(), distro.codename())
else:
os_version = (platform.release(),)
@ -355,27 +353,30 @@ def show_file(path, timestamp=None):
timestamp,
timestamp,
)
if dbus:
bus = dbus.SessionBus()
filemanager1 = bus.get_object(
'org.freedesktop.FileManager1', '/org/freedesktop/FileManager1'
)
paths = [urljoin('file:', pathname2url(path))]
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')
# No option in xdg to highlight a file so just open parent folder.
subprocess.Popen(['xdg-open', os.path.dirname(path.rstrip('/'))], env=env)
try:
filemanager1 = bus.get_object(DBUS_FM_ID, DBUS_FM_PATH)
except dbus.exceptions.DBusException as ex:
log.debug('Unable to get dbus file manager: %s', ex)
# Fallback to xdg-open
else:
paths = [urljoin('file:', pathname2url(path))]
filemanager1.ShowItems(paths, startup_id, dbus_interface=DBUS_FM_ID)
return
env = os.environ.copy()
env['DESKTOP_STARTUP_ID'] = startup_id.replace('dbus', 'xdg-open')
# No option in xdg to highlight a file so just open parent folder.
subprocess.Popen(['xdg-open', os.path.dirname(path.rstrip('/'))], env=env)
def open_url_in_browser(url):
"""
Opens a url in the desktop's default browser
Opens a URL in the desktop's default browser
:param url: the url to open
:param url: the URL to open
:type url: string
"""
@ -430,27 +431,27 @@ def fsize(fsize_b, precision=1, shortform=False):
'110 KiB'
Note:
This function has been refactored for perfomance with the
This function has been refactored for performance with the
fsize units being translated outside the function.
"""
if fsize_b >= 1024 ** 4:
if fsize_b >= 1024**4:
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 4,
fsize_b / 1024**4,
tib_txt_short if shortform else tib_txt,
)
elif fsize_b >= 1024 ** 3:
elif fsize_b >= 1024**3:
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 3,
fsize_b / 1024**3,
gib_txt_short if shortform else gib_txt,
)
elif fsize_b >= 1024 ** 2:
elif fsize_b >= 1024**2:
return '%.*f %s' % (
precision,
fsize_b / 1024 ** 2,
fsize_b / 1024**2,
mib_txt_short if shortform else mib_txt,
)
elif fsize_b >= 1024:
@ -502,28 +503,28 @@ def fspeed(bps, precision=1, shortform=False):
"""
if bps < 1024 ** 2:
if bps < 1024**2:
return '%.*f %s' % (
precision,
bps / 1024,
_('K/s') if shortform else _('KiB/s'),
)
elif bps < 1024 ** 3:
elif bps < 1024**3:
return '%.*f %s' % (
precision,
bps / 1024 ** 2,
bps / 1024**2,
_('M/s') if shortform else _('MiB/s'),
)
elif bps < 1024 ** 4:
elif bps < 1024**4:
return '%.*f %s' % (
precision,
bps / 1024 ** 3,
bps / 1024**3,
_('G/s') if shortform else _('GiB/s'),
)
else:
return '%.*f %s' % (
precision,
bps / 1024 ** 4,
bps / 1024**4,
_('T/s') if shortform else _('TiB/s'),
)
@ -546,9 +547,9 @@ def fpeer(num_peers, total_peers):
"""
if total_peers > -1:
return '{:d} ({:d})'.format(num_peers, total_peers)
return f'{num_peers:d} ({total_peers:d})'
else:
return '{:d}'.format(num_peers)
return f'{num_peers:d}'
def ftime(secs):
@ -565,7 +566,7 @@ def ftime(secs):
'6h 23m'
Note:
This function has been refactored for perfomance.
This function has been refactored for performance.
"""
@ -574,17 +575,17 @@ def ftime(secs):
if secs <= 0:
time_str = ''
elif secs < 60:
time_str = '{}s'.format(secs)
time_str = f'{secs}s'
elif secs < 3600:
time_str = '{}m {}s'.format(secs // 60, secs % 60)
time_str = f'{secs // 60}m {secs % 60}s'
elif secs < 86400:
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60)
time_str = f'{secs // 3600}h {secs // 60 % 60}m'
elif secs < 604800:
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24)
time_str = f'{secs // 86400}d {secs // 3600 % 24}h'
elif secs < 31449600:
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7)
time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
else:
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52)
time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
return time_str
@ -638,17 +639,17 @@ def tokenize(text):
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},
{'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},
]
@ -695,7 +696,7 @@ def is_url(url):
"""
A simple test to check if the URL is valid
:param url: the url to test
:param url: the URL to test
:type url: string
:returns: True or False
:rtype: bool
@ -706,6 +707,9 @@ def is_url(url):
True
"""
if not url:
return False
return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp')
@ -720,6 +724,9 @@ def is_infohash(infohash):
bool: True if valid infohash, False otherwise.
"""
if not infohash:
return False
return len(infohash) == 40 and infohash.isalnum()
@ -727,13 +734,15 @@ def is_infohash(infohash):
XT_BTIH_PARAM = 'xt=urn:btih:'
DN_PARAM = 'dn='
TR_PARAM = 'tr='
TR_TIER_PARAM = 'tr.'
TR_TIER_REGEX = re.compile(r'^tr.(\d+)=(\S+)')
def is_magnet(uri):
"""
A check to determine if a uri is a valid bittorrent magnet uri
A check to determine if a URI is a valid bittorrent magnet URI
:param uri: the uri to check
:param uri: the URI to check
:type uri: string
:returns: True or False
:rtype: bool
@ -769,8 +778,6 @@ def get_magnet_info(uri):
"""
tr0_param = 'tr.'
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
if not uri.startswith(MAGNET_SCHEME):
return {}
@ -798,12 +805,14 @@ def get_magnet_info(uri):
tracker = unquote_plus(param[len(TR_PARAM) :])
trackers[tracker] = tier
tier += 1
elif param.startswith(tr0_param):
try:
tier, tracker = re.match(tr0_param_regex, param).groups()
trackers[tracker] = tier
except AttributeError:
pass
elif param.startswith(TR_TIER_PARAM):
tracker_match = re.match(TR_TIER_REGEX, param)
if not tracker_match:
continue
tier, tracker = tracker_match.groups()
tracker = unquote_plus(tracker)
trackers[tracker] = int(tier)
if info_hash:
if not name:
@ -819,7 +828,7 @@ def get_magnet_info(uri):
def create_magnet_uri(infohash, name=None, trackers=None):
"""Creates a magnet uri
"""Creates a magnet URI
Args:
infohash (str): The info-hash of the torrent.
@ -827,7 +836,7 @@ def create_magnet_uri(infohash, name=None, trackers=None):
trackers (list or dict, optional): A list of trackers or dict or {tracker: tier} pairs.
Returns:
str: A magnet uri string.
str: A magnet URI string.
"""
try:
@ -898,6 +907,29 @@ def free_space(path):
return disk_data.f_bavail * block_size
def is_interface(interface):
"""Check if interface is a valid IP or network adapter.
Args:
interface (str): The IP or interface name to test.
Returns:
bool: Whether interface is valid is not.
Examples:
Windows:
>>> is_interface('{7A30AE62-23ZA-3744-Z844-A5B042524871}')
>>> is_interface('127.0.0.1')
True
Linux:
>>> is_interface('lo')
>>> is_interface('127.0.0.1')
True
"""
return is_ip(interface) or is_interface_name(interface)
def is_ip(ip):
"""A test to see if 'ip' is a valid IPv4 or IPv6 address.
@ -933,15 +965,12 @@ def is_ipv4(ip):
"""
import socket
try:
if windows_check():
return socket.inet_aton(ip)
else:
return socket.inet_pton(socket.AF_INET, ip)
except socket.error:
socket.inet_pton(socket.AF_INET, ip)
except OSError:
return False
else:
return True
def is_ipv6(ip):
@ -960,23 +989,51 @@ def is_ipv6(ip):
"""
try:
import ipaddress
except ImportError:
import socket
try:
return socket.inet_pton(socket.AF_INET6, ip)
except (socket.error, AttributeError):
if windows_check():
log.warning('Unable to verify IPv6 Address on Windows.')
return True
socket.inet_pton(socket.AF_INET6, ip)
except OSError:
return False
else:
try:
return ipaddress.IPv6Address(decode_bytes(ip))
except ipaddress.AddressValueError:
pass
return True
return False
def is_interface_name(name):
"""Returns True if an interface name exists.
Args:
name (str): The Interface to test. eg. eth0 linux. GUID on Windows.
Returns:
bool: Whether name is valid or not.
Examples:
>>> is_interface_name("eth0")
True
>>> is_interface_name("{7A30AE62-23ZA-3744-Z844-A5B042524871}")
True
"""
if not windows_check():
try:
socket.if_nametoindex(name)
except OSError:
pass
else:
return True
if ifaddr:
try:
adapters = ifaddr.get_adapters()
except OSError:
return True
else:
return any([name == a.name for a in adapters])
if windows_check():
regex = '^{[0-9A-Z]{8}-([0-9A-Z]{4}-){3}[0-9A-Z]{12}}$'
return bool(re.search(regex, str(name)))
return True
def decode_bytes(byte_str, encoding='utf8'):
@ -1007,9 +1064,9 @@ def decode_bytes(byte_str, encoding='utf8'):
if encoding.lower() not in ['utf8', 'utf-8']:
encodings.insert(0, lambda: (encoding, 'strict'))
for l in encodings:
for enc in encodings:
try:
return byte_str.decode(*l())
return byte_str.decode(*enc())
except UnicodeDecodeError:
pass
return ''
@ -1054,7 +1111,7 @@ def utf8_encode_structure(data):
@functools.total_ordering
class VersionSplit(object):
class VersionSplit:
"""
Used for comparing version numbers.
@ -1138,6 +1195,7 @@ def __lt__(self, other):
def create_auth_file():
import stat
import deluge.configmanager
auth_file = deluge.configmanager.get_config_dir('auth')
@ -1153,6 +1211,7 @@ def create_auth_file():
def create_localclient_account(append=False):
import random
from hashlib import sha1 as sha
import deluge.configmanager
auth_file = deluge.configmanager.get_config_dir('auth')
@ -1175,7 +1234,7 @@ def create_localclient_account(append=False):
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.
@ -1231,15 +1290,10 @@ def set_env_variable(name, value):
http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py
"""
# Update Python's copy of the environment variables
try:
os.environ[name] = value
except UnicodeEncodeError:
# Python 2
os.environ[name] = value.encode('utf8')
os.environ[name] = value
if windows_check():
from ctypes import windll
from ctypes import cdll
from ctypes import cdll, windll
# Update the copy maintained by Windows (so SysInternals Process Explorer sees it)
result = windll.kernel32.SetEnvironmentVariableW(name, value)
@ -1255,56 +1309,13 @@ def set_env_variable(name, value):
)
# Update the copy maintained by msvcrt (used by gtk+ runtime)
result = cdll.msvcrt._wputenv('%s=%s' % (name, value))
result = cdll.msvcrt._wputenv(f'{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)
def unicode_argv():
""" Gets sys.argv as list of unicode objects on any platform."""
if windows_check():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
get_cmd_linew = cdll.kernel32.GetCommandLineW
get_cmd_linew.argtypes = []
get_cmd_linew.restype = LPCWSTR
cmdline_to_argvw = windll.shell32.CommandLineToArgvW
cmdline_to_argvw.argtypes = [LPCWSTR, POINTER(c_int)]
cmdline_to_argvw.restype = POINTER(LPWSTR)
cmd = get_cmd_linew()
argc = c_int(0)
argv = cmdline_to_argvw(cmd, byref(argc))
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)]
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
encoding = getattr(sys.stdout, 'encoding') or getattr(sys.stdin, 'encoding')
# If that fails, check what the locale is set to
encoding = encoding or locale.getpreferredencoding()
# As a last resort, just default to utf-8
encoding = encoding or 'utf-8'
arg_list = []
for arg in sys.argv:
try:
arg_list.append(arg.decode(encoding))
except AttributeError:
arg_list.append(arg)
return arg_list
def run_profiled(func, *args, **kwargs):
"""
Profile a function with cProfile

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
#
@ -7,13 +6,10 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
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
@ -27,13 +23,13 @@ class ComponentAlreadyRegistered(Exception):
class ComponentException(Exception):
def __init__(self, message, tb):
super(ComponentException, self).__init__(message)
super().__init__(message)
self.message = message
self.tb = tb
def __str__(self):
s = super(ComponentException, self).__str__()
return '%s\n%s' % (s, ''.join(self.tb))
s = super().__str__()
return '{}\n{}'.format(s, ''.join(self.tb))
def __eq__(self, other):
if isinstance(other, self.__class__):
@ -45,7 +41,7 @@ def __ne__(self, other):
return not self.__eq__(other)
class Component(object):
class Component:
"""Component objects are singletons managed by the :class:`ComponentRegistry`.
When a new Component object is instantiated, it will be automatically
@ -250,7 +246,7 @@ def shutdown(self):
pass
class ComponentRegistry(object):
class ComponentRegistry:
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.
It is used to manage the Components by starting, stopping, pausing and shutting them down.
@ -293,7 +289,8 @@ def deregister(self, obj):
obj (Component): a component object to deregister
Returns:
Deferred: a deferred object that will fire once the Component has been sucessfully deregistered
Deferred: a deferred object that will fire once the Component has been
successfully deregistered
"""
if obj in self.components.values():
@ -324,7 +321,7 @@ def start(self, names=None):
# Start all the components if names is empty
if not names:
names = list(self.components)
elif isinstance(names, string_types):
elif isinstance(names, str):
names = [names]
def on_depends_started(result, name):
@ -358,7 +355,7 @@ def stop(self, names=None):
"""
if not names:
names = list(self.components)
elif isinstance(names, string_types):
elif isinstance(names, str):
names = [names]
def on_dependents_stopped(result, name):
@ -398,7 +395,7 @@ def pause(self, names=None):
"""
if not names:
names = list(self.components)
elif isinstance(names, string_types):
elif isinstance(names, str):
names = [names]
deferreds = []
@ -424,7 +421,7 @@ def resume(self, names=None):
"""
if not names:
names = list(self.components)
elif isinstance(names, string_types):
elif isinstance(names, str):
names = [names]
deferreds = []
@ -448,7 +445,7 @@ def shutdown(self):
def on_stopped(result):
return DeferredList(
[comp._component_shutdown() for comp in self.components.values()]
[comp._component_shutdown() for comp in list(self.components.values())]
)
return self.stop(list(self.components)).addCallback(on_stopped)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
#
@ -39,78 +38,66 @@
version as this will be done internally.
"""
from __future__ import unicode_literals
import json
import logging
import os
import pickle
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 # noqa: N816 Necessary for the config tests
def prop(func):
"""Function decorator for defining property attributes
The decorated function is expected to return a dictionary
containing one or more of the following pairs:
fget - function for getting attribute value
fset - function for setting attribute value
fdel - function for deleting attribute
This can be conveniently constructed by the locals() builtin
function; see:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
"""
return property(doc=func.__doc__, **func())
def find_json_objects(s):
"""Find json objects in a string.
def find_json_objects(text, decoder=json.JSONDecoder()):
"""Find json objects in text.
Args:
s (str): the string to find json objects in
text (str): The text to find json objects within.
Returns:
list: A list of tuples containing start and end locations of json
objects in string `s`. e.g. [(start, end), ...]
objects in the text. e.g. [(start, end), ...]
"""
objects = []
opens = 0
start = s.find('{')
offset = start
offset = 0
while True:
try:
start = text.index('{', offset)
except ValueError:
break
if start < 0:
return []
quoted = False
for index, c in enumerate(s[offset:]):
if c == '"':
quoted = not quoted
elif quoted:
continue
elif c == '{':
opens += 1
elif c == '}':
opens -= 1
if opens == 0:
objects.append((start, index + offset + 1))
start = index + offset + 1
try:
__, index = decoder.raw_decode(text[start:])
except json.decoder.JSONDecodeError:
offset = start + 1
else:
offset = start + index
objects.append((start, offset))
return objects
class Config(object):
def cast_to_existing_type(value, old_value):
"""Attempt to convert new value type to match old value type"""
types_match = isinstance(old_value, (type(None), type(value)))
if value is not None and not types_match:
old_type = type(old_value)
# Skip convert to bytes since requires knowledge of encoding and value should
# be unicode anyway.
if old_type is bytes:
return value
return old_type(value)
return value
class Config:
"""This class is used to access/create/modify config files.
Args:
@ -120,13 +107,23 @@ class Config(object):
file_version (int): The file format for the default config values when creating
a fresh config. This value should be increased whenever a new migration function is
setup to convert old config files. (default: 1)
log_mask_funcs (dict): A dict of key:function, used to mask sensitive
key values (e.g. passwords) when logging is enabled.
"""
def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
def __init__(
self,
filename,
defaults=None,
config_dir=None,
file_version=1,
log_mask_funcs=None,
):
self.__config = {}
self.__set_functions = {}
self.__change_callbacks = []
self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {}
# These hold the version numbers and they will be set when loaded
self.__version = {'format': 1, 'file': file_version}
@ -137,7 +134,7 @@ def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
if defaults:
for key, value in defaults.items():
self.set_item(key, value)
self.set_item(key, value, default=True)
# Load the config from file in the config_dir
if config_dir:
@ -147,6 +144,12 @@ def __init__(self, filename, defaults=None, config_dir=None, file_version=1):
self.load()
def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase
"""Wrapper around reactor.callLater for test purpose."""
from twisted.internet import reactor
return reactor.callLater(period, func, *args, **kwargs)
def __contains__(self, item):
return item in self.__config
@ -155,7 +158,7 @@ def __setitem__(self, key, value):
return self.set_item(key, value)
def set_item(self, key, value):
def set_item(self, key, value, default=False):
"""Sets item 'key' to 'value' in the config dictionary.
Does not allow changing the item's type unless it is None.
@ -167,6 +170,8 @@ def set_item(self, key, value):
key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is
currently in the config.
default (optional, bool): When setting a default value skip func or save
callbacks.
Raises:
ValueError: Raised when the type of value is not the same as what is
@ -179,61 +184,54 @@ def set_item(self, key, value):
5
"""
if key not in self.__config:
self.__config[key] = value
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
return
if isinstance(value, bytes):
value = value.decode()
if self.__config[key] == value:
return
# 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:
if key in self.__config:
try:
oldtype = type(self.__config[key])
# Don't convert to bytes as requires encoding and value will
# be decoded anyway.
if oldtype is not bytes:
value = oldtype(value)
value = cast_to_existing_type(value, self.__config[key])
except ValueError:
log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise
else:
if self.__config[key] == value:
return
if isinstance(value, bytes):
value = value.decode('utf8')
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
if log.isEnabledFor(logging.DEBUG):
if key in self.__log_mask_funcs:
value = self.__log_mask_funcs[key](value)
log.debug(
'Setting key "%s" to: %s (of type: %s)',
key,
value,
type(value),
)
self.__config[key] = value
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import (
callLater,
) # pylint: disable=redefined-outer-name
# Skip save or func callbacks if setting default value for keys
if default:
return
# Run the set_function for this key if any
try:
for func in self.__set_functions[key]:
callLater(0, func, key, value)
except KeyError:
pass
for func in self.__set_functions.get(key, []):
self.callLater(0, func, key, value)
try:
def do_change_callbacks(key, value):
for func in self.__change_callbacks:
func(key, value)
callLater(0, do_change_callbacks, key, value)
self.callLater(0, do_change_callbacks, key, value)
except Exception:
pass
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save)
self._save_timer = self.callLater(5, self.save)
def __getitem__(self, key):
"""See get_item """
"""See get_item"""
return self.get_item(key)
def get_item(self, key):
@ -306,16 +304,9 @@ def del_item(self, key):
del self.__config[key]
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import (
callLater,
) # pylint: disable=redefined-outer-name
# We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save)
self._save_timer = self.callLater(5, self.save)
def register_change_callback(self, callback):
"""Registers a callback function for any changed value.
@ -361,7 +352,6 @@ def register_set_function(self, key, function, apply_now=True):
# Run the function now if apply_now is set
if apply_now:
function(key, self.__config[key])
return
def apply_all(self):
"""Calls all set functions.
@ -404,9 +394,9 @@ def load(self, filename=None):
filename = self.__config_file
try:
with open(filename, 'r', encoding='utf8') as _file:
with open(filename, encoding='utf8') as _file:
data = _file.read()
except IOError as ex:
except OSError as ex:
log.warning('Unable to open config file %s: %s', filename, ex)
return
@ -436,12 +426,24 @@ def load(self, filename=None):
log.exception(ex)
log.warning('Unable to load config file: %s', filename)
if not log.isEnabledFor(logging.DEBUG):
return
config = self.__config
if self.__log_mask_funcs:
config = {
key: self.__log_mask_funcs[key](config[key])
if key in self.__log_mask_funcs
else config[key]
for key in config
}
log.debug(
'Config %s version: %s.%s loaded: %s',
filename,
self.__version['format'],
self.__version['file'],
self.__config,
config,
)
def save(self, filename=None):
@ -459,7 +461,7 @@ def save(self, filename=None):
# Check to see if the current config differs from the one on disk
# We will only write a new config file if there is a difference
try:
with open(filename, 'r', encoding='utf8') as _file:
with open(filename, encoding='utf8') as _file:
data = _file.read()
objects = find_json_objects(data)
start, end = objects[0]
@ -471,7 +473,7 @@ def save(self, filename=None):
if self._save_timer and self._save_timer.active():
self._save_timer.cancel()
return True
except (IOError, IndexError) as ex:
except (OSError, IndexError) as ex:
log.warning('Unable to open config file: %s because: %s', filename, ex)
# Save the new config and make sure it's written to disk
@ -485,7 +487,7 @@ def save(self, filename=None):
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
_file.flush()
os.fsync(_file.fileno())
except IOError as ex:
except OSError as ex:
log.error('Error writing new config file: %s', ex)
return False
@ -496,7 +498,7 @@ def save(self, filename=None):
try:
log.debug('Backing up old config file to %s.bak', filename)
shutil.move(filename, filename + '.bak')
except IOError as ex:
except OSError as ex:
log.warning('Unable to backup old config: %s', ex)
# The new config file has been written successfully, so let's move it over
@ -504,7 +506,7 @@ def save(self, filename=None):
try:
log.debug('Moving new config file %s to %s', filename_tmp, filename)
shutil.move(filename_tmp, filename)
except IOError as ex:
except OSError as ex:
log.error('Error moving new config file: %s', ex)
return False
else:
@ -556,14 +558,11 @@ def run_converter(self, input_range, output_version, func):
def config_file(self):
return self.__config_file
@prop
def config(): # pylint: disable=no-method-argument
@property
def config(self):
"""The config dictionary"""
return self.__config
def fget(self):
return self.__config
def fdel(self):
return self.save()
return locals()
@config.deleter
def config(self):
return self.save()

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -7,8 +6,6 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import os
@ -19,7 +16,7 @@
log = logging.getLogger(__name__)
class _ConfigManager(object):
class _ConfigManager:
def __init__(self):
log.debug('ConfigManager started..')
self.config_files = {}

192
deluge/conftest.py Normal file
View file

@ -0,0 +1,192 @@
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import tempfile
import warnings
from unittest.mock import Mock, patch
import pytest
import pytest_twisted
from twisted.internet import reactor
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.error import CannotListenError
from twisted.python.failure import Failure
import deluge.component as _component
import deluge.configmanager
from deluge.common import get_localhost_auth
from deluge.tests import common
from deluge.ui.client import client as _client
DEFAULT_LISTEN_PORT = 58900
@pytest.fixture
def listen_port(request):
if request and 'daemon' in request.fixturenames:
try:
return request.getfixturevalue('daemon').listen_port
except Exception:
pass
return DEFAULT_LISTEN_PORT
@pytest.fixture
def mock_callback():
"""Returns a `Mock` object which can be registered as a callback to test against.
If callback was not called within `timeout` seconds, it will raise a TimeoutError.
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
"""
def reset():
if mock.called:
original_reset_mock()
deferred = Deferred()
deferred.addTimeout(0.5, reactor)
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
mock.deferred = deferred
mock = Mock()
original_reset_mock = mock.reset_mock
mock.reset_mock = reset
mock.reset_mock()
return mock
@pytest.fixture
def config_dir(tmp_path):
deluge.configmanager.set_config_dir(tmp_path)
yield tmp_path
@pytest_twisted.async_yield_fixture()
async def client(request, config_dir, monkeypatch, listen_port):
# monkeypatch.setattr(
# _client, 'connect', functools.partial(_client.connect, port=listen_port)
# )
try:
username, password = get_localhost_auth()
except Exception:
username, password = '', ''
await _client.connect(
'localhost',
port=listen_port,
username=username,
password=password,
)
yield _client
if _client.connected():
await _client.disconnect()
@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir):
listen_port = DEFAULT_LISTEN_PORT
logfile = f'daemon_{request.node.name}.log'
if hasattr(request.cls, 'daemon_custom_script'):
custom_script = request.cls.daemon_custom_script
else:
custom_script = ''
for dummy in range(10):
try:
d, daemon = common.start_core(
listen_port=listen_port,
logfile=logfile,
timeout=5,
timeout_msg='Timeout!',
custom_script=custom_script,
print_stdout=True,
print_stderr=True,
config_directory=config_dir,
)
await d
except CannotListenError as ex:
exception_error = ex
listen_port += 1
except (KeyboardInterrupt, SystemExit):
raise
else:
break
else:
raise exception_error
daemon.listen_port = listen_port
yield daemon
await daemon.kill()
@pytest.fixture(autouse=True)
def common_fixture(config_dir, request, monkeypatch, listen_port):
"""Adds some instance attributes to test classes for backwards compatibility with old testing."""
def fail(self, reason):
if isinstance(reason, Failure):
reason = reason.value
return pytest.fail(str(reason))
if request.instance:
request.instance.patch = monkeypatch.setattr
request.instance.config_dir = config_dir
request.instance.listen_port = listen_port
request.instance.id = lambda: request.node.name
request.cls.fail = fail
@pytest_twisted.async_yield_fixture(scope='function')
async def component(request):
"""Verify component registry is clean, and clean up after test."""
if len(_component._ComponentRegistry.components) != 0:
warnings.warn(
'The component._ComponentRegistry.components is not empty on test setup.\n'
'This is probably caused by another test that did not clean up after finishing!: %s'
% _component._ComponentRegistry.components
)
yield _component
await _component.shutdown()
_component._ComponentRegistry.components.clear()
_component._ComponentRegistry.dependents.clear()
@pytest_twisted.async_yield_fixture(scope='function')
async def base_fixture(common_fixture, component, request):
"""This fixture is autoused on all tests that subclass BaseTestCase"""
self = request.instance
if hasattr(self, 'set_up'):
try:
await maybeDeferred(self.set_up)
except Exception as exc:
warnings.warn('Error caught in test setup!\n%s' % exc)
pytest.fail('Error caught in test setup!\n%s' % exc)
yield
if hasattr(self, 'tear_down'):
try:
await maybeDeferred(self.tear_down)
except Exception as exc:
pytest.fail('Error caught in test teardown!\n%s' % exc)
@pytest.mark.usefixtures('base_fixture')
class BaseTestCase:
"""This is the base class that should be used for all test classes
that create classes that inherit from deluge.component.Component. It
ensures that the component registry has been cleaned up when tests
have finished.
"""
@pytest.fixture
def mock_mkstemp(tmp_path):
"""Return known tempfile location to verify file deleted"""
tmp_file = tempfile.mkstemp(dir=tmp_path)
with patch('tempfile.mkstemp', return_value=tmp_file):
yield tmp_file

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -15,10 +14,8 @@
`:mod:EventManager` for similar functionality.
"""
from __future__ import unicode_literals
import logging
import types
from types import SimpleNamespace
from twisted.internet import reactor
@ -28,14 +25,6 @@
log = logging.getLogger(__name__)
try:
SimpleNamespace = types.SimpleNamespace # Python 3.3+
except AttributeError:
class SimpleNamespace(object): # Python 2.7
def __init__(self, **attr):
self.__dict__.update(attr)
class AlertManager(component.Component):
"""AlertManager fetches and processes libtorrent alerts"""
@ -57,6 +46,7 @@ def __init__(self):
| lt.alert.category_t.status_notification
| lt.alert.category_t.ip_block_notification
| lt.alert.category_t.performance_warning
| lt.alert.category_t.file_progress_notification
)
self.session.apply_settings({'alert_mask': alert_mask})

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -8,12 +7,9 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import os
import shutil
from io import open
import deluge.component as component
import deluge.configmanager as configmanager
@ -32,14 +28,14 @@
AUTH_LEVELS_MAPPING = {
'NONE': AUTH_LEVEL_NONE,
'READONLY': AUTH_LEVEL_READONLY,
'DEFAULT': AUTH_LEVEL_NORMAL,
'NORMAL': AUTH_LEVEL_DEFAULT,
'DEFAULT': AUTH_LEVEL_DEFAULT,
'NORMAL': AUTH_LEVEL_NORMAL,
'ADMIN': AUTH_LEVEL_ADMIN,
}
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
class Account(object):
class Account:
__slots__ = ('username', 'password', 'authlevel')
def __init__(self, username, password, authlevel):
@ -56,10 +52,10 @@ def data(self):
}
def __repr__(self):
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % {
'username': self.username,
'authlevel': self.authlevel,
}
return '<Account username="{username}" authlevel={authlevel}>'.format(
username=self.username,
authlevel=self.authlevel,
)
class AuthManager(component.Component):
@ -101,7 +97,7 @@ def authorize(self, username, password):
int: The auth level for this user.
Raises:
AuthenticationRequired: If aditional details are required to authenticate.
AuthenticationRequired: If additional details are required to authenticate.
BadLoginError: If the username does not exist or password does not match.
"""
@ -184,7 +180,7 @@ def write_auth_file(self):
if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak)
except IOError as ex:
except OSError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else:
log.info('Saving the %s at: %s', filename, filepath)
@ -198,7 +194,7 @@ def write_auth_file(self):
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
except IOError as ex:
except OSError as ex:
log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
@ -227,9 +223,9 @@ def __load_auth_file(self):
for _filepath in (auth_file, auth_file_bak):
log.info('Opening %s for load: %s', filename, _filepath)
try:
with open(_filepath, 'r', encoding='utf8') as _file:
with open(_filepath, encoding='utf8') as _file:
file_data = _file.readlines()
except IOError as ex:
except OSError as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
file_data = []
else:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -8,8 +7,6 @@
# See LICENSE for more details.
#
from __future__ import division, unicode_literals
import glob
import logging
import os
@ -17,8 +14,9 @@
import tempfile
import threading
from base64 import b64decode, b64encode
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.request import URLError, urlopen
from six import string_types
from twisted.internet import defer, reactor, task
from twisted.web.client import Agent, readBody
@ -41,7 +39,7 @@
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.decorators import deprecated, maybe_coroutine
from deluge.error import (
AddTorrentError,
DelugeError,
@ -56,12 +54,6 @@
)
from deluge.httpdownloader import download_file
try:
from urllib.request import urlopen, URLError
except ImportError:
# PY2 fallback
from urllib2 import urlopen, URLError
log = logging.getLogger(__name__)
DEPR_SESSION_STATUS_KEYS = {
@ -120,7 +112,7 @@ def __init__(
component.Component.__init__(self, 'Core')
# Start the libtorrent session.
user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION)
user_agent = f'Deluge/{DELUGE_VER} libtorrent/{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 = {
@ -173,19 +165,25 @@ def __init__(
# store the one in the config so we can restore it on shutdown
self._old_listen_interface = None
if listen_interface:
if deluge.common.is_ip(listen_interface):
if deluge.common.is_interface(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',
'Invalid listen interface (must be IP Address or Interface Name): %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
if deluge.common.is_interface(outgoing_interface):
self._old_outgoing_interface = self.config['outgoing_interface']
self.config['outgoing_interface'] = outgoing_interface
else:
log.error(
'Invalid outgoing interface (must be IP Address or Interface Name): %s',
outgoing_interface,
)
# New release check information
self.__new_release = None
@ -243,13 +241,12 @@ def apply_session_settings(self, settings):
"""Apply libtorrent session settings.
Args:
settings (dict): A dict of lt session settings to apply.
settings: A dict of lt session settings to apply.
"""
self.session.apply_settings(settings)
@staticmethod
def _create_peer_id(version):
def _create_peer_id(version: str) -> str:
"""Create a peer_id fingerprint.
This creates the peer_id and modifies the release char to identify
@ -264,11 +261,10 @@ def _create_peer_id(version):
``--DE201b--`` (beta pre-release of v2.0.1)
Args:
version (str): The version string in PEP440 dotted notation.
version: The version string in PEP440 dotted notation.
Returns:
str: The formattted peer_id with Deluge prefix e.g. '--DE200s--'
The formatted 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.
@ -301,7 +297,7 @@ def _save_session_state(self):
if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak)
except IOError as ex:
except OSError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else:
log.info('Saving the %s at: %s', filename, filepath)
@ -311,18 +307,17 @@ def _save_session_state(self):
_file.flush()
os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath)
except (IOError, EOFError) as ex:
except (OSError, EOFError) as ex:
log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak)
shutil.move(filepath_bak, filepath)
def _load_session_state(self):
def _load_session_state(self) -> dict:
"""Loads the libtorrent session state
Returns:
dict: A libtorrent sesion state, empty dict if unable to load it.
A libtorrent sesion state, empty dict if unable to load it.
"""
filename = 'session.state'
filepath = get_config_dir(filename)
@ -333,7 +328,7 @@ def _load_session_state(self):
try:
with open(_filepath, 'rb') as _file:
state = lt.bdecode(_file.read())
except (IOError, EOFError, RuntimeError) as ex:
except (OSError, EOFError, RuntimeError) as ex:
log.warning('Unable to load %s: %s', _filepath, ex)
else:
log.info('Successfully loaded %s: %s', filename, _filepath)
@ -358,8 +353,8 @@ def _update_session_cache_hit_ratio(self):
if blocks_read:
self.session_status['read_hit_ratio'] = (
self.session_status['disk.num_blocks_cache_hits'] / blocks_read
)
blocks_read - self.session_status['disk.num_read_ops']
) / blocks_read
else:
self.session_status['read_hit_ratio'] = 0.0
@ -404,18 +399,19 @@ def check_new_release(self):
# Exported Methods
@export
def add_torrent_file_async(self, filename, filedump, options, save_state=True):
"""Adds a torrent file to the session asynchonously.
def add_torrent_file_async(
self, filename: str, filedump: str, options: dict, save_state: bool = True
) -> 'defer.Deferred[Optional[str]]':
"""Adds a torrent file to the session asynchronously.
Args:
filename (str): The filename of the torrent.
filedump (str): A base64 encoded string of torrent file contents.
options (dict): The options to apply to the torrent upon adding.
save_state (bool): If the state should be saved after adding the file.
filename: The filename of the torrent.
filedump: A base64 encoded string of torrent file contents.
options: The options to apply to the torrent upon adding.
save_state: If the state should be saved after adding the file.
Returns:
Deferred: The torrent ID or None.
The torrent ID or None.
"""
try:
filedump = b64decode(filedump)
@ -436,42 +432,39 @@ def add_torrent_file_async(self, filename, filedump, options, save_state=True):
return d
@export
def prefetch_magnet_metadata(self, magnet, timeout=30):
@maybe_coroutine
async def prefetch_magnet_metadata(
self, magnet: str, timeout: int = 30
) -> Tuple[str, bytes]:
"""Download magnet metadata without adding to Deluge session.
Used by UIs to get magnet files for selection before adding to session.
The metadata is bencoded and for transfer base64 encoded.
Args:
magnet (str): The magnet uri.
timeout (int): Number of seconds to wait before cancelling request.
magnet: The magnet URI.
timeout: Number of seconds to wait before canceling request.
Returns:
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet.
A tuple of (torrent_id, metadata) 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
return await self.torrentmanager.prefetch_metadata(magnet, timeout)
@export
def add_torrent_file(self, filename, filedump, options):
def add_torrent_file(
self, filename: str, filedump: Union[str, bytes], options: dict
) -> Optional[str]:
"""Adds a torrent file to the session.
Args:
filename (str): The filename of the torrent.
filedump (str): A base64 encoded string of the torrent file contents.
options (dict): The options to apply to the torrent upon adding.
filename: The filename of the torrent.
filedump: A base64 encoded string of the torrent file contents.
options: The options to apply to the torrent upon adding.
Returns:
str: The torrent_id or None.
The torrent_id or None.
"""
try:
filedump = b64decode(filedump)
@ -487,24 +480,26 @@ def add_torrent_file(self, filename, filedump, options):
raise
@export
def add_torrent_files(self, torrent_files):
"""Adds multiple torrent files to the session asynchonously.
def add_torrent_files(
self, torrent_files: List[Tuple[str, Union[str, bytes], dict]]
) -> 'defer.Deferred[List[AddTorrentError]]':
"""Adds multiple torrent files to the session asynchronously.
Args:
torrent_files (list of tuples): Torrent files as tuple of (filename, filedump, options).
torrent_files: Torrent files as tuple of
``(filename, filedump, options)``.
Returns:
Deferred
A list of errors (if there were any)
"""
@defer.inlineCallbacks
def add_torrents():
@maybe_coroutine
async def add_torrents():
errors = []
last_index = len(torrent_files) - 1
for idx, torrent in enumerate(torrent_files):
try:
yield self.add_torrent_file_async(
await self.add_torrent_file_async(
torrent[0], torrent[1], torrent[2], save_state=idx == last_index
)
except AddTorrentError as ex:
@ -515,93 +510,89 @@ def add_torrents():
return task.deferLater(reactor, 0, add_torrents)
@export
def add_torrent_url(self, url, options, headers=None):
@maybe_coroutine
async def add_torrent_url(
self, url: str, options: dict, headers: dict = None
) -> 'defer.Deferred[Optional[str]]':
"""Adds a torrent from a URL. Deluge will attempt to fetch the torrent
from the URL prior to adding it to the session.
Args:
url: the URL pointing to the torrent file
options: the options to apply to the torrent on add
headers: any optional headers to send
Returns:
a Deferred which returns the torrent_id as a str or None
"""
Adds a torrent from a url. Deluge will attempt to fetch the torrent
from url prior to adding it to the session.
:param url: the url pointing to the torrent file
:type url: string
:param options: the options to apply to the torrent on add
:type options: dict
:param headers: any optional headers to send
:type headers: dict
:returns: a Deferred which returns the torrent_id as a str or None
"""
log.info('Attempting to add url %s', url)
def on_download_success(filename):
# We got the file, so add it to the session
with open(filename, 'rb') as _file:
data = _file.read()
try:
os.remove(filename)
except OSError as ex:
log.warning('Could not remove temp file: %s', ex)
return self.add_torrent_file(filename, b64encode(data), options)
def on_download_fail(failure):
# Log the error and pass the failure onto the client
log.error('Failed to add torrent from url %s', url)
return failure
log.info('Attempting to add URL %s', url)
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
os.close(tmp_fd)
d = download_file(url, tmp_file, headers=headers, force_filename=True)
d.addCallbacks(on_download_success, on_download_fail)
return d
try:
filename = await download_file(
url, tmp_file, headers=headers, force_filename=True
)
except Exception:
log.error('Failed to add torrent from URL %s', url)
raise
else:
with open(filename, 'rb') as _file:
data = _file.read()
return self.add_torrent_file(filename, b64encode(data), options)
finally:
try:
os.close(tmp_fd)
os.remove(tmp_file)
except OSError as ex:
log.warning(f'Unable to delete temp file {tmp_file}: , {ex}')
@export
def add_torrent_magnet(self, uri, options):
def add_torrent_magnet(self, uri: str, options: dict) -> str:
"""Adds a torrent from a magnet link.
Args:
uri: the magnet link
options: the options to apply to the torrent on add
Returns:
the torrent_id
"""
Adds a torrent from a magnet link.
:param uri: the magnet link
:type uri: string
:param options: the options to apply to the torrent on add
:type options: dict
:returns: the torrent_id
:rtype: string
"""
log.debug('Attempting to add by magnet uri: %s', uri)
log.debug('Attempting to add by magnet URI: %s', uri)
return self.torrentmanager.add(magnet=uri, options=options)
@export
def remove_torrent(self, torrent_id, remove_data):
def remove_torrent(self, torrent_id: str, remove_data: bool) -> bool:
"""Removes a single torrent from the session.
Args:
torrent_id (str): The torrent ID to remove.
remove_data (bool): If True, also remove the downloaded data.
torrent_id: The torrent ID to remove.
remove_data: If True, also remove the downloaded data.
Returns:
bool: True if removed successfully.
True if removed successfully.
Raises:
InvalidTorrentError: If the torrent ID does not exist in the session.
"""
log.debug('Removing torrent %s from the core.', torrent_id)
return self.torrentmanager.remove(torrent_id, remove_data)
@export
def remove_torrents(self, torrent_ids, remove_data):
def remove_torrents(
self, torrent_ids: List[str], remove_data: bool
) -> 'defer.Deferred[List[Tuple[str, str]]]':
"""Remove multiple torrents from the session.
Args:
torrent_ids (list): The torrent IDs to remove.
remove_data (bool): If True, also remove the downloaded data.
torrent_ids: The torrent IDs to remove.
remove_data: If True, also remove the downloaded data.
Returns:
list: An empty list if no errors occurred otherwise the list contains
tuples of strings, a torrent ID and an error message. For example:
[('<torrent_id>', 'Error removing torrent')]
An empty list if no errors occurred otherwise the list contains
tuples of strings, a torrent ID and an error message. For example:
[('<torrent_id>', 'Error removing torrent')]
"""
log.info('Removing %d torrents from core.', len(torrent_ids))
@ -625,17 +616,17 @@ def do_remove_torrents():
return task.deferLater(reactor, 0, do_remove_torrents)
@export
def get_session_status(self, keys):
def get_session_status(self, keys: List[str]) -> Dict[str, Union[int, float]]:
"""Gets the session status values for 'keys', these keys are taking
from libtorrent's session status.
See: http://www.rasterbar.com/products/libtorrent/manual.html#status
:param keys: the keys for which we want values
:type keys: list
:returns: a dictionary of {key: value, ...}
:rtype: dict
Args:
keys: the keys for which we want values
Returns:
a dictionary of {key: value, ...}
"""
if not keys:
return self.session_status
@ -652,26 +643,26 @@ def get_session_status(self, keys):
)
status[key] = self.session_status[new_key]
else:
log.warning('Session status key not valid: %s', key)
log.debug('Session status key not valid: %s', key)
return status
@export
def force_reannounce(self, torrent_ids):
def force_reannounce(self, torrent_ids: List[str]) -> None:
log.debug('Forcing reannouncment to: %s', torrent_ids)
for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_reannounce()
@export
def pause_torrent(self, torrent_id):
def pause_torrent(self, torrent_id: str) -> None:
"""Pauses a torrent"""
log.debug('Pausing: %s', torrent_id)
if not isinstance(torrent_id, string_types):
if not isinstance(torrent_id, str):
self.pause_torrents(torrent_id)
else:
self.torrentmanager[torrent_id].pause()
@export
def pause_torrents(self, torrent_ids=None):
def pause_torrents(self, torrent_ids: List[str] = None) -> None:
"""Pauses a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
@ -679,27 +670,27 @@ def pause_torrents(self, torrent_ids=None):
self.pause_torrent(torrent_id)
@export
def connect_peer(self, torrent_id, ip, port):
def connect_peer(self, torrent_id: str, ip: str, port: int):
log.debug('adding peer %s to %s', ip, torrent_id)
if not self.torrentmanager[torrent_id].connect_peer(ip, port):
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
@export
def move_storage(self, torrent_ids, dest):
def move_storage(self, torrent_ids: List[str], dest: str):
log.debug('Moving storage %s to %s', torrent_ids, dest)
for torrent_id in torrent_ids:
if not self.torrentmanager[torrent_id].move_storage(dest):
log.warning('Error moving torrent %s to %s', torrent_id, dest)
@export
def pause_session(self):
def pause_session(self) -> None:
"""Pause the entire session"""
if not self.session.is_paused():
self.session.pause()
component.get('EventManager').emit(SessionPausedEvent())
@export
def resume_session(self):
def resume_session(self) -> None:
"""Resume the entire session"""
if self.session.is_paused():
self.session.resume()
@ -708,21 +699,21 @@ def resume_session(self):
component.get('EventManager').emit(SessionResumedEvent())
@export
def is_session_paused(self):
def is_session_paused(self) -> bool:
"""Returns the activity of the session"""
return self.session.is_paused()
@export
def resume_torrent(self, torrent_id):
def resume_torrent(self, torrent_id: str) -> None:
"""Resumes a torrent"""
log.debug('Resuming: %s', torrent_id)
if not isinstance(torrent_id, string_types):
if not isinstance(torrent_id, str):
self.resume_torrents(torrent_id)
else:
self.torrentmanager[torrent_id].resume()
@export
def resume_torrents(self, torrent_ids=None):
def resume_torrents(self, torrent_ids: List[str] = None) -> None:
"""Resumes a list of torrents"""
if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list()
@ -746,7 +737,7 @@ def create_torrent_status(
import traceback
traceback.print_exc()
# Torrent was probaly removed meanwhile
# Torrent was probably removed meanwhile
return {}
# Ask the plugin manager to fill in the plugin keys
@ -755,7 +746,9 @@ def create_torrent_status(
return status
@export
def get_torrent_status(self, torrent_id, keys, diff=False):
def get_torrent_status(
self, torrent_id: str, keys: List[str], diff: bool = False
) -> dict:
torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
keys, [torrent_id]
)
@ -769,57 +762,54 @@ def get_torrent_status(self, torrent_id, keys, diff=False):
)
@export
def get_torrents_status(self, filter_dict, keys, diff=False):
"""
returns all torrents , optionally filtered by filter_dict.
"""
@maybe_coroutine
async def get_torrents_status(
self, filter_dict: dict, keys: List[str], diff: bool = False
) -> dict:
"""returns all torrents , optionally filtered by filter_dict."""
all_keys = not keys
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff)
def add_plugin_fields(args):
status_dict, plugin_keys = args
# 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)
)
return status_dict
d.addCallback(add_plugin_fields)
return d
status_dict, plugin_keys = await self.torrentmanager.torrents_status_update(
torrent_ids, keys, diff=diff
)
# Ask the plugin manager to fill in the plugin keys
if len(plugin_keys) > 0 or all_keys:
for key in status_dict:
status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
return status_dict
@export
def get_filter_tree(self, show_zero_hits=True, hide_cat=None):
"""
returns {field: [(value,count)] }
def get_filter_tree(
self, show_zero_hits: bool = True, hide_cat: List[str] = None
) -> Dict:
"""returns {field: [(value,count)] }
for use in sidebar(s)
"""
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
@export
def get_session_state(self):
def get_session_state(self) -> List[str]:
"""Returns a list of torrent_ids in the session."""
# Get the torrent list from the TorrentManager
return self.torrentmanager.get_torrent_list()
@export
def get_config(self):
def get_config(self) -> dict:
"""Get all the preferences as a dictionary"""
return self.config.config
@export
def get_config_value(self, key):
def get_config_value(self, key: str) -> Any:
"""Get the config value for key"""
return self.config.get(key)
@export
def get_config_values(self, keys):
def get_config_values(self, keys: List[str]) -> Dict[str, Any]:
"""Get the config values for the entered keys"""
return {key: self.config.get(key) for key in keys}
@export
def set_config(self, config):
def set_config(self, config: Dict[str, Any]):
"""Set the config with values from dictionary"""
# Load all the values into the configuration
for key in config:
@ -828,21 +818,20 @@ def set_config(self, config):
self.config[key] = config[key]
@export
def get_listen_port(self):
def get_listen_port(self) -> int:
"""Returns the active listen port"""
return self.session.listen_port()
@export
def get_proxy(self):
def get_proxy(self) -> Dict[str, Any]:
"""Returns the proxy settings
Returns:
dict: Contains proxy settings.
Proxy settings.
Notes:
Proxy type names:
0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P
"""
settings = self.session.get_settings()
@ -865,51 +854,60 @@ def get_proxy(self):
return proxy_dict
@export
def get_available_plugins(self):
def get_available_plugins(self) -> List[str]:
"""Returns a list of plugins available in the core"""
return self.pluginmanager.get_available_plugins()
@export
def get_enabled_plugins(self):
def get_enabled_plugins(self) -> List[str]:
"""Returns a list of enabled plugins in the core"""
return self.pluginmanager.get_enabled_plugins()
@export
def enable_plugin(self, plugin):
def enable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
return self.pluginmanager.enable_plugin(plugin)
@export
def disable_plugin(self, plugin):
def disable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
return self.pluginmanager.disable_plugin(plugin)
@export
def force_recheck(self, torrent_ids):
def force_recheck(self, torrent_ids: List[str]) -> None:
"""Forces a data recheck on torrent_ids"""
for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_recheck()
@export
def set_torrent_options(self, torrent_ids, options):
def set_torrent_options(
self, torrent_ids: List[str], options: Dict[str, Any]
) -> None:
"""Sets the torrent options for torrent_ids
Args:
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.
torrent_ids: A list of torrent_ids to set the options for.
options: A dict of torrent options to set. See
``torrent.TorrentOptions`` class for valid keys.
"""
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, string_types):
if isinstance(torrent_ids, str):
torrent_ids = [torrent_ids]
for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].set_options(options)
@export
def set_torrent_trackers(self, torrent_id, trackers):
"""Sets a torrents tracker list. trackers will be [{"url", "tier"}]"""
def set_torrent_trackers(
self, torrent_id: str, trackers: List[Dict[str, Any]]
) -> None:
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
return self.torrentmanager[torrent_id].set_trackers(trackers)
@export
def get_magnet_uri(self, torrent_id: str) -> str:
return self.torrentmanager[torrent_id].get_magnet_uri()
@deprecated
@export
def set_torrent_max_connections(self, torrent_id, value):
@ -985,7 +983,7 @@ def set_torrent_move_completed_path(self, torrent_id, value):
@export
def get_path_size(self, path):
"""Returns the size of the file or folder 'path' and -1 if the path is
unaccessible (non-existent or insufficient privs)"""
inaccessible (non-existent or insufficient privileges)"""
return deluge.common.get_path_size(path)
@export
@ -1055,11 +1053,11 @@ def _create_torrent_thread(
self.add_torrent_file(os.path.split(target)[1], filedump, options)
@export
def upload_plugin(self, filename, filedump):
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
"""This method is used to upload new plugins to the daemon. It is used
when connecting to the daemon remotely and installing a new plugin on
the client side. 'plugin_data' is a xmlrpc.Binary object of the file data,
ie, plugin_file.read()"""
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
i.e. ``plugin_file.read()``"""
try:
filedump = b64decode(filedump)
@ -1073,26 +1071,24 @@ def upload_plugin(self, filename, filedump):
component.get('CorePluginManager').scan_for_plugins()
@export
def rescan_plugins(self):
"""
Rescans the plugin folders for new plugins
"""
def rescan_plugins(self) -> None:
"""Re-scans the plugin folders for new plugins"""
component.get('CorePluginManager').scan_for_plugins()
@export
def rename_files(self, torrent_id, filenames):
"""
Rename files in torrent_id. Since this is an asynchronous operation by
def rename_files(
self, torrent_id: str, filenames: List[Tuple[int, str]]
) -> defer.Deferred:
"""Rename files in ``torrent_id``. Since this is an asynchronous operation by
libtorrent, watch for the TorrentFileRenamedEvent to know when the
files have been renamed.
:param torrent_id: the torrent_id to rename files
:type torrent_id: string
:param filenames: a list of index, filename pairs
:type filenames: ((index, filename), ...)
:raises InvalidTorrentError: if torrent_id is invalid
Args:
torrent_id: the torrent_id to rename files
filenames: a list of index, filename pairs
Raises:
InvalidTorrentError: if torrent_id is invalid
"""
if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session')
@ -1103,21 +1099,20 @@ def rename():
return task.deferLater(reactor, 0, rename)
@export
def rename_folder(self, torrent_id, folder, new_folder):
"""
Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
def rename_folder(
self, torrent_id: str, folder: str, new_folder: str
) -> defer.Deferred:
"""Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
TorrentFolderRenamedEvent which is emitted when the folder has been
renamed successfully.
:param torrent_id: the torrent to rename folder in
:type torrent_id: string
:param folder: the folder to rename
:type folder: string
:param new_folder: the new folder name
:type new_folder: string
:raises InvalidTorrentError: if the torrent_id is invalid
Args:
torrent_id: the torrent to rename folder in
folder: the folder to rename
new_folder: the new folder name
Raises:
InvalidTorrentError: if the torrent_id is invalid
"""
if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session')
@ -1125,7 +1120,7 @@ def rename_folder(self, torrent_id, folder, new_folder):
return self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
@export
def queue_top(self, torrent_ids):
def queue_top(self, torrent_ids: List[str]) -> None:
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(
@ -1139,7 +1134,7 @@ def queue_top(self, torrent_ids):
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export
def queue_up(self, torrent_ids):
def queue_up(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to up', torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1164,7 +1159,7 @@ def queue_up(self, torrent_ids):
prev_queue_position = queue_position
@export
def queue_down(self, torrent_ids):
def queue_down(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to down', torrent_ids)
torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1189,7 +1184,7 @@ def queue_down(self, torrent_ids):
prev_queue_position = queue_position
@export
def queue_bottom(self, torrent_ids):
def queue_bottom(self, torrent_ids: List[str]) -> None:
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(
@ -1203,17 +1198,15 @@ def queue_bottom(self, torrent_ids):
log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export
def glob(self, path):
def glob(self, path: str) -> List[str]:
return glob.glob(path)
@export
def test_listen_port(self):
"""
Checks if the active port is open
:returns: True if the port is open, False if not
:rtype: bool
def test_listen_port(self) -> 'defer.Deferred[Optional[bool]]':
"""Checks if the active port is open
Returns:
True if the port is open, False if not
"""
port = self.get_listen_port()
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
@ -1232,18 +1225,17 @@ def on_error(failure):
return d
@export
def get_free_space(self, path=None):
"""
Returns the number of free bytes at path
def get_free_space(self, path: str = None) -> int:
"""Returns the number of free bytes at path
:param path: the path to check free space at, if None, use the default download location
:type path: string
Args:
path: the path to check free space at, if None, use the default download location
:returns: the number of free bytes at path
:rtype: int
:raises InvalidPathError: if the path is invalid
Returns:
the number of free bytes at path
Raises:
InvalidPathError: if the path is invalid
"""
if not path:
path = self.config['download_location']
@ -1256,46 +1248,40 @@ def _on_external_ip_event(self, external_ip):
self.external_ip = external_ip
@export
def get_external_ip(self):
"""
Returns the external ip address recieved from libtorrent.
"""
def get_external_ip(self) -> str:
"""Returns the external IP address received from libtorrent."""
return self.external_ip
@export
def get_libtorrent_version(self):
"""
Returns the libtorrent version.
:returns: the version
:rtype: string
def get_libtorrent_version(self) -> str:
"""Returns the libtorrent version.
Returns:
the version
"""
return LT_VERSION
@export
def get_completion_paths(self, args):
"""
Returns the available path completions for the input value.
"""
def get_completion_paths(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""Returns the available path completions for the input value."""
return path_chooser_common.get_completion_paths(args)
@export(AUTH_LEVEL_ADMIN)
def get_known_accounts(self):
def get_known_accounts(self) -> List[Dict[str, Any]]:
return self.authmanager.get_known_accounts()
@export(AUTH_LEVEL_NONE)
def get_auth_levels_mappings(self):
def get_auth_levels_mappings(self) -> Tuple[Dict[str, int], Dict[int, str]]:
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
@export(AUTH_LEVEL_ADMIN)
def create_account(self, username, password, authlevel):
def create_account(self, username: str, password: str, authlevel: str) -> bool:
return self.authmanager.create_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def update_account(self, username, password, authlevel):
def update_account(self, username: str, password: str, authlevel: str) -> bool:
return self.authmanager.update_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN)
def remove_account(self, username):
def remove_account(self, username: str) -> bool:
return self.authmanager.remove_account(username)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -8,8 +7,6 @@
#
"""The Deluge daemon"""
from __future__ import unicode_literals
import logging
import os
import socket
@ -44,8 +41,8 @@ def is_daemon_running(pid_file):
try:
with open(pid_file) as _file:
pid, port = [int(x) for x in _file.readline().strip().split(';')]
except (EnvironmentError, ValueError):
pid, port = (int(x) for x in _file.readline().strip().split(';'))
except (OSError, ValueError):
return False
if is_process_running(pid):
@ -53,7 +50,7 @@ def is_daemon_running(pid_file):
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
_socket.connect(('127.0.0.1', port))
except socket.error:
except OSError:
# Can't connect, so pid is not a deluged process.
return False
else:
@ -62,7 +59,7 @@ def is_daemon_running(pid_file):
return True
class Daemon(object):
class Daemon:
"""The Deluge Daemon class"""
def __init__(
@ -156,7 +153,7 @@ def start(self):
pid = os.getpid()
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
with open(self.pid_file, 'w') as _file:
_file.write('%s;%s\n' % (pid, self.port))
_file.write(f'{pid};{self.port}\n')
component.start()
@ -200,6 +197,7 @@ def authorized_call(self, rpc):
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

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
@ -7,8 +6,6 @@
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
from __future__ import print_function, unicode_literals
import os
import sys
from logging import DEBUG, FileHandler, getLogger

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,8 +6,6 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
import deluge.component as component

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
#
@ -7,12 +6,8 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import logging
from six import string_types
import deluge.component as component
from deluge.common import TORRENT_STATE
@ -100,9 +95,7 @@ def tracker_error_filter(torrent_ids, values):
class FilterManager(component.Component):
"""FilterManager
"""
"""FilterManager"""
def __init__(self, core):
component.Component.__init__(self, 'FilterManager')
@ -138,7 +131,7 @@ def filter_torrent_ids(self, filter_dict):
# Sanitize input: filter-value must be a list of strings
for key, value in filter_dict.items():
if isinstance(value, string_types):
if isinstance(value, str):
filter_dict[key] = [value]
# Optimized filter for id

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
#
@ -9,8 +8,6 @@
"""PluginManager for Core"""
from __future__ import unicode_literals
import logging
from twisted.internet import defer

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
#
@ -8,13 +7,13 @@
#
from __future__ import unicode_literals
import logging
import os
import platform
import random
import threading
from urllib.parse import quote_plus
from urllib.request import urlopen
from twisted.internet.task import LoopingCall
@ -24,17 +23,14 @@
from deluge._libtorrent import lt
from deluge.event import ConfigValueChangedEvent
GeoIP = None
try:
import GeoIP
from GeoIP import GeoIP
except ImportError:
GeoIP = None
try:
from urllib.parse import quote_plus
from urllib.request import urlopen
except ImportError:
from urllib import quote_plus
from urllib2 import urlopen
try:
from pygeoip import GeoIP
except ImportError:
pass
log = logging.getLogger(__name__)
@ -202,7 +198,7 @@ def _on_set_random_port(self, key, value):
self.__set_listen_on()
def __set_listen_on(self):
""" Set the ports and interface address to listen for incoming connections on."""
"""Set the ports and interface address to listen for incoming connections on."""
if self.config['random_port']:
if not self.config['listen_random_port']:
self.config['listen_random_port'] = random.randrange(49152, 65525)
@ -225,13 +221,13 @@ def __set_listen_on(self):
self.config['listen_use_sys_port'],
)
interfaces = [
'%s:%s' % (interface, port)
f'{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_interfaces': ','.join(interfaces),
}
)
@ -400,7 +396,7 @@ def run(self):
+ quote_plus(':'.join(self.config['enabled_plugins']))
)
urlopen(url)
except IOError as ex:
except OSError as ex:
log.debug('Network error while trying to send info: %s', ex)
else:
self.config['info_sent'] = now
@ -464,11 +460,9 @@ def _on_set_geoip_db_location(self, key, geoipdb_path):
# 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
)
except AttributeError:
log.warning('GeoIP Unavailable')
self.core.geoip_instance = GeoIP(geoipdb_path, 0)
except Exception as ex:
log.warning('GeoIP Unavailable: %s', ex)
else:
log.warning('Unable to find GeoIP database file: %s', geoipdb_path)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com>
#
@ -8,17 +7,14 @@
#
"""RPCServer Module"""
from __future__ import unicode_literals
import logging
import os
import stat
import sys
import traceback
from collections import namedtuple
from types import FunctionType
from typing import Callable, TypeVar, overload
from OpenSSL import crypto
from twisted.internet import defer, reactor
from twisted.internet.protocol import Factory, connectionDone
@ -29,7 +25,7 @@
AUTH_LEVEL_DEFAULT,
AUTH_LEVEL_NONE,
)
from deluge.crypto_utils import get_context_factory
from deluge.crypto_utils import check_ssl_keys, get_context_factory
from deluge.error import (
DelugeError,
IncompatibleClient,
@ -46,6 +42,18 @@
log = logging.getLogger(__name__)
TCallable = TypeVar('TCallable', bound=Callable)
@overload
def export(func: TCallable) -> TCallable:
...
@overload
def export(auth_level: int) -> Callable[[TCallable], TCallable]:
...
def export(auth_level=AUTH_LEVEL_DEFAULT):
"""
@ -69,7 +77,7 @@ def wrap(func, *args, **kwargs):
if func.__doc__:
if func.__doc__.endswith(' '):
indent = func.__doc__.split('\n')[-1]
func.__doc__ += '\n{}'.format(indent)
func.__doc__ += f'\n{indent}'
else:
func.__doc__ += '\n\n'
func.__doc__ += rpc_text
@ -114,7 +122,7 @@ def format_request(call):
class DelugeRPCProtocol(DelugeTransferProtocol):
def __init__(self):
super(DelugeRPCProtocol, self).__init__()
super().__init__()
# namedtuple subclass with auth_level, username for the connected session.
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
@ -588,59 +596,3 @@ def emit_event_for_session_id(self, session_id, event):
def stop(self):
self.factory.state = 'stopping'
def check_ssl_keys():
"""
Check for SSL cert/key and create them if necessary
"""
ssl_dir = deluge.configmanager.get_config_dir('ssl')
if not os.path.exists(ssl_dir):
# The ssl folder doesn't exist so we need to create it
os.makedirs(ssl_dir)
generate_ssl_keys()
else:
for f in ('daemon.pkey', 'daemon.cert'):
if not os.path.exists(os.path.join(ssl_dir, f)):
generate_ssl_keys()
break
def generate_ssl_keys():
"""
This method generates a new SSL key/cert.
"""
from deluge.common import PY2
digest = 'sha256' if not PY2 else b'sha256'
# Generate key pair
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
# Generate cert request
req = crypto.X509Req()
subj = req.get_subject()
setattr(subj, 'CN', 'Deluge Daemon')
req.set_pubkey(pkey)
req.sign(pkey, digest)
# Generate certificate
cert = crypto.X509()
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, digest)
# Write out files
ssl_dir = deluge.configmanager.get_config_dir('ssl')
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
# Make the files only readable by this user
for f in ('daemon.pkey', 'daemon.cert'):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -14,11 +13,12 @@
"""
from __future__ import division, unicode_literals
import logging
import os
import socket
import time
from typing import Optional
from urllib.parse import urlparse
from twisted.internet.defer import Deferred, DeferredList
@ -34,18 +34,6 @@
TorrentTrackerStatusEvent,
)
try:
from urllib.parse import urlparse
except ImportError:
# PY2 fallback
from urlparse import urlparse # pylint: disable=ungrouped-imports
try:
from future_builtins import zip
except ImportError:
# Ignore on Py3.
pass
log = logging.getLogger(__name__)
LT_TORRENT_STATE_MAP = {
@ -94,7 +82,7 @@ def convert_lt_files(files):
"""Indexes and decodes files from libtorrent get_files().
Args:
files (list): The libtorrent torrent files.
files (file_storage): The libtorrent torrent files.
Returns:
list of dict: The files.
@ -109,18 +97,18 @@ def convert_lt_files(files):
}
"""
filelist = []
for index, _file in enumerate(files):
for index in range(files.num_files()):
try:
file_path = _file.path.decode('utf8')
file_path = files.file_path(index).decode('utf8')
except AttributeError:
file_path = _file.path
file_path = files.file_path(index)
filelist.append(
{
'index': index,
'path': file_path.replace('\\', '/'),
'size': _file.size,
'offset': _file.offset,
'size': files.file_size(index),
'offset': files.file_offset(index),
}
)
@ -161,7 +149,7 @@ class TorrentOptions(dict):
"""
def __init__(self):
super(TorrentOptions, self).__init__()
super().__init__()
config = ConfigManager('core.conf').config
options_conf_map = {
'add_paused': 'add_paused',
@ -191,14 +179,14 @@ def __init__(self):
self['seed_mode'] = False
class TorrentError(object):
class TorrentError:
def __init__(self, error_message, was_paused=False, restart_to_resume=False):
self.error_message = error_message
self.was_paused = was_paused
self.restart_to_resume = restart_to_resume
class Torrent(object):
class Torrent:
"""Torrent holds information about torrents added to the libtorrent session.
Args:
@ -206,12 +194,12 @@ class Torrent(object):
options (dict): The torrent options.
state (TorrentState): The torrent state.
filename (str): The filename of the torrent file.
magnet (str): The magnet uri.
magnet (str): The magnet URI.
Attributes:
torrent_id (str): The torrent_id for this torrent
handle: Holds the libtorrent torrent handle
magnet (str): The magnet uri used to add this torrent (if available).
magnet (str): The magnet URI used to add this torrent (if available).
status: Holds status info so that we don"t need to keep getting it from libtorrent.
torrent_info: store the torrent info.
has_metadata (bool): True if the metadata for the torrent is available, False otherwise.
@ -248,9 +236,10 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None):
self.handle = handle
self.magnet = magnet
self.status = self.handle.status()
self._status: Optional['lt.torrent_status'] = None
self._status_last_update: float = 0.0
self.torrent_info = self.handle.get_torrent_info()
self.torrent_info = self.handle.torrent_file()
self.has_metadata = self.status.has_metadata
self.options = TorrentOptions()
@ -266,6 +255,9 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None):
self.is_finished = False
self.filename = filename
if not self.filename:
self.filename = ''
self.forced_error = None
self.statusmsg = None
self.state = None
@ -278,7 +270,6 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None):
self.prev_status = {}
self.waiting_on_folder_rename = []
self.update_status(self.handle.status())
self._create_status_funcs()
self.set_options(self.options)
self.update_state()
@ -286,6 +277,18 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None):
if log.isEnabledFor(logging.DEBUG):
log.debug('Torrent object created.')
def _set_handle_flags(self, flag: lt.torrent_flags, set_flag: bool):
"""set or unset a flag to the lt handle
Args:
flag (lt.torrent_flags): the flag to set/unset
set_flag (bool): True for setting the flag, False for unsetting it
"""
if set_flag:
self.handle.set_flags(flag)
else:
self.handle.unset_flags(flag)
def on_metadata_received(self):
"""Process the metadata received alert for this torrent"""
self.has_metadata = True
@ -370,7 +373,7 @@ def set_max_download_speed(self, m_down_speed):
"""Sets maximum download speed for this torrent.
Args:
m_up_speed (float): Maximum download speed in KiB/s.
m_down_speed (float): Maximum download speed in KiB/s.
"""
self.options['max_download_speed'] = m_down_speed
if m_down_speed < 0:
@ -402,7 +405,7 @@ def set_prioritize_first_last_pieces(self, prioritize):
return
# A list of priorities for each piece in the torrent
priorities = self.handle.piece_priorities()
priorities = self.handle.get_piece_priorities()
def get_file_piece(idx, byte_offset):
return self.torrent_info.map_file(idx, byte_offset, 0).piece
@ -428,14 +431,17 @@ def get_file_piece(idx, byte_offset):
# Setting the priorites for all the pieces of this torrent
self.handle.prioritize_pieces(priorities)
def set_sequential_download(self, set_sequencial):
def set_sequential_download(self, sequential):
"""Sets whether to download the pieces of the torrent in order.
Args:
set_sequencial (bool): Enable sequencial downloading.
sequential (bool): Enable sequential downloading.
"""
self.options['sequential_download'] = set_sequencial
self.handle.set_sequential_download(set_sequencial)
self.options['sequential_download'] = sequential
self._set_handle_flags(
flag=lt.torrent_flags.sequential_download,
set_flag=sequential,
)
def set_auto_managed(self, auto_managed):
"""Set auto managed mode, i.e. will be started or queued automatically.
@ -445,7 +451,10 @@ def set_auto_managed(self, auto_managed):
"""
self.options['auto_managed'] = auto_managed
if not (self.status.paused and not self.status.auto_managed):
self.handle.auto_managed(auto_managed)
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=auto_managed,
)
self.update_state()
def set_super_seeding(self, super_seeding):
@ -455,7 +464,10 @@ def set_super_seeding(self, super_seeding):
super_seeding (bool): Enable super seeding.
"""
self.options['super_seeding'] = super_seeding
self.handle.super_seeding(super_seeding)
self._set_handle_flags(
flag=lt.torrent_flags.super_seeding,
set_flag=super_seeding,
)
def set_stop_ratio(self, stop_ratio):
"""The seeding ratio to stop (or remove) the torrent at.
@ -516,7 +528,7 @@ def set_file_priorities(self, file_priorities):
self.handle.prioritize_files(file_priorities)
else:
log.debug('Unable to set new file priorities.')
file_priorities = self.handle.file_priorities()
file_priorities = self.handle.get_file_priorities()
if 0 in self.options['file_priorities']:
# Previously marked a file 'skip' so check for any 0's now >0.
@ -566,7 +578,7 @@ def set_trackers(self, trackers=None):
trackers (list of dicts): A list of trackers.
"""
if trackers is None:
self.trackers = [tracker for tracker in self.handle.trackers()]
self.trackers = list(self.handle.trackers())
self.tracker_host = None
return
@ -631,7 +643,7 @@ def merge_trackers(self, torrent_info):
def update_state(self):
"""Updates the state, based on libtorrent's torrent state"""
status = self.handle.status()
status = self.get_lt_status()
session_paused = component.get('Core').session.is_paused()
old_state = self.state
self.set_status_message()
@ -643,7 +655,10 @@ def update_state(self):
elif status_error:
self.state = 'Error'
# auto-manage status will be reverted upon resuming.
self.handle.auto_managed(False)
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.set_status_message(decode_bytes(status_error))
elif status.moving_storage:
self.state = 'Moving'
@ -696,8 +711,11 @@ def force_error_state(self, message, restart_to_resume=True):
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
session can resume.
"""
status = self.handle.status()
self.handle.auto_managed(False)
status = self.get_lt_status()
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
if not status.paused:
self.handle.pause()
@ -711,7 +729,10 @@ def clear_forced_error_state(self, update_state=True):
log.error('Restart deluge to clear this torrent error')
if not self.forced_error.was_paused and self.options['auto_managed']:
self.handle.auto_managed(True)
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
self.forced_error = None
self.set_status_message('OK')
if update_state:
@ -810,7 +831,11 @@ def get_peers(self):
if peer.flags & peer.connecting or peer.flags & peer.handshake:
continue
client = decode_bytes(peer.client)
try:
client = decode_bytes(peer.client)
except UnicodeDecodeError:
# libtorrent on Py3 can raise UnicodeDecodeError for peer_info.client
client = 'unknown'
try:
country = component.get('Core').geoip_instance.country_code_by_addr(
@ -831,7 +856,7 @@ def get_peers(self):
'client': client,
'country': country,
'down_speed': peer.payload_down_speed,
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]),
'ip': f'{peer.ip[0]}:{peer.ip[1]}',
'progress': peer.progress,
'seed': peer.flags & peer.seed,
'up_speed': peer.payload_up_speed,
@ -850,7 +875,7 @@ def get_queue_position(self):
def get_file_priorities(self):
"""Return the file priorities"""
if not self.handle.has_metadata():
if not self.handle.status().has_metadata:
return []
if not self.options['file_priorities']:
@ -867,11 +892,18 @@ def get_file_progress(self):
"""
if not self.has_metadata:
return []
return [
progress / _file.size if _file.size else 0.0
for progress, _file in zip(
try:
files_progresses = zip(
self.handle.file_progress(), self.torrent_info.files()
)
except Exception:
# Handle libtorrent >=2.0.0,<=2.0.4 file_progress error
files_progresses = zip(iter(lambda: 0, 1), self.torrent_info.files())
return [
progress / _file.size if _file.size else 0.0
for progress, _file in files_progresses
]
def get_tracker_host(self):
@ -896,7 +928,7 @@ def get_tracker_host(self):
# Check if hostname is an IP address and just return it if that's the case
try:
socket.inet_aton(host)
except socket.error:
except OSError:
pass
else:
# This is an IP address because an exception wasn't raised
@ -913,7 +945,7 @@ def get_tracker_host(self):
return ''
def get_magnet_uri(self):
"""Returns a magnet uri for this torrent"""
"""Returns a magnet URI for this torrent"""
return lt.make_magnet_uri(self.handle)
def get_name(self):
@ -932,10 +964,10 @@ def get_name(self):
if self.has_metadata:
# Use the top-level folder as torrent name.
filename = decode_bytes(self.torrent_info.file_at(0).path)
filename = decode_bytes(self.torrent_info.files().file_path(0))
name = filename.replace('\\', '/', 1).split('/', 1)[0]
else:
name = decode_bytes(self.handle.name())
name = decode_bytes(self.handle.status().name)
if not name:
name = self.torrent_id
@ -987,12 +1019,14 @@ def get_status(self, keys, diff=False, update=False, all_keys=False):
call to get_status based on the session_id
update (bool): If True the status will be updated from libtorrent
if False, the cached values will be returned
all_keys (bool): If True return all keys while ignoring the keys param
if False, return only the requested keys
Returns:
dict: a dictionary of the status keys and their values
"""
if update:
self.update_status(self.handle.status())
self.get_lt_status()
if all_keys:
keys = list(self.status_funcs)
@ -1022,13 +1056,35 @@ def get_status(self, keys, diff=False, update=False, all_keys=False):
return status_dict
def update_status(self, status):
def get_lt_status(self) -> 'lt.torrent_status':
"""Get the torrent status fresh, not from cache.
This should be used when a guaranteed fresh status is needed rather than
`torrent.handle.status()` because it will update the cache as well.
"""
self.status = self.handle.status()
return self.status
@property
def status(self) -> 'lt.torrent_status':
"""Cached copy of the libtorrent status for this torrent.
If it has not been updated within the last five seconds, it will be
automatically refreshed.
"""
if self._status_last_update < (time.time() - 5):
self.status = self.handle.status()
return self._status
@status.setter
def status(self, status: 'lt.torrent_status') -> None:
"""Updates the cached status.
Args:
status (libtorrent.torrent_status): a libtorrent torrent status
status: a libtorrent torrent status
"""
self.status = status
self._status = status
self._status_last_update = time.time()
def _create_status_funcs(self):
"""Creates the functions for getting torrent status"""
@ -1150,7 +1206,10 @@ def pause(self):
"""
# Turn off auto-management so the torrent will not be unpaused by lt queueing
self.handle.auto_managed(False)
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
if self.state == 'Error':
log.debug('Unable to pause torrent while in Error state')
elif self.status.paused:
@ -1185,7 +1244,10 @@ def resume(self):
else:
# Check if torrent was originally being auto-managed.
if self.options['auto_managed']:
self.handle.auto_managed(True)
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
try:
self.handle.resume()
except RuntimeError as ex:
@ -1208,8 +1270,8 @@ def connect_peer(self, peer_ip, peer_port):
bool: True is successful, otherwise False
"""
try:
self.handle.connect_peer((peer_ip, peer_port), 0)
except RuntimeError as ex:
self.handle.connect_peer((peer_ip, int(peer_port)), 0)
except (RuntimeError, ValueError) as ex:
log.debug('Unable to connect to peer: %s', ex)
return False
return True
@ -1289,7 +1351,7 @@ def write_file(filepath, filedump):
try:
with open(filepath, 'wb') as save_file:
save_file.write(filedump)
except IOError as ex:
except OSError as ex:
log.error('Unable to save torrent file to: %s', ex)
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
@ -1312,7 +1374,7 @@ def delete_torrentfile(self, delete_copies=False):
torrent_files = [
os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')
]
if delete_copies:
if delete_copies and self.filename:
torrent_files.append(
os.path.join(self.config['torrentfiles_location'], self.filename)
)
@ -1336,8 +1398,8 @@ def force_reannounce(self):
def scrape_tracker(self):
"""Scrape the tracker
A scrape request queries the tracker for statistics such as total
number of incomplete peers, complete peers, number of downloads etc.
A scrape request queries the tracker for statistics such as total
number of incomplete peers, complete peers, number of downloads etc.
"""
try:
self.handle.scrape_tracker()
@ -1384,7 +1446,7 @@ def rename_folder(self, folder, new_folder):
This basically does a file rename on all of the folders children.
Args:
folder (str): The orignal folder name
folder (str): The original folder name
new_folder (str): The new folder name
Returns:

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
#
@ -8,27 +7,33 @@
#
"""TorrentManager handles Torrent objects"""
from __future__ import unicode_literals
import datetime
import logging
import operator
import os
import pickle
import time
from collections import namedtuple
from base64 import b64encode
from tempfile import gettempdir
from typing import Dict, List, NamedTuple, Tuple
import six.moves.cPickle as pickle # noqa: N813
from twisted.internet import defer, error, reactor, threads
from twisted.internet import defer, reactor, threads
from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.task import LoopingCall
import deluge.component as component
from deluge._libtorrent import lt
from deluge.common import archive_files, decode_bytes, get_magnet_info, is_magnet
from deluge._libtorrent import LT_VERSION, lt
from deluge.common import (
VersionSplit,
archive_files,
decode_bytes,
get_magnet_info,
is_magnet,
)
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath
from deluge.decorators import maybe_coroutine
from deluge.error import AddTorrentError, InvalidTorrentError
from deluge.event import (
ExternalIPEvent,
@ -52,6 +57,11 @@
)
class PrefetchQueueItem(NamedTuple):
alert_deferred: Deferred
result_queue: List[Deferred]
class TorrentState: # pylint: disable=old-style-class
"""Create a torrent state.
@ -89,7 +99,7 @@ def __init__(
super_seeding=False,
name=None,
):
# Build the class atrribute list from args
# Build the class attribute list from args
for key, value in locals().items():
if key == 'self':
continue
@ -129,7 +139,8 @@ class TorrentManager(component.Component):
"""
callLater = reactor.callLater # noqa: N815
# This is used in the test to mock out timeouts
clock = reactor
def __init__(self):
component.Component.__init__(
@ -158,7 +169,7 @@ def __init__(self):
self.is_saving_state = False
self.save_resume_data_file_lock = defer.DeferredLock()
self.torrents_loading = {}
self.prefetching_metadata = {}
self.prefetching_metadata: Dict[str, PrefetchQueueItem] = {}
# This is a map of torrent_ids to Deferreds used to track needed resume data.
# The Deferreds will be completed when resume data has been saved.
@ -243,8 +254,8 @@ def start(self):
self.save_resume_data_timer.start(190, False)
self.prev_status_cleanup_loop.start(10)
@defer.inlineCallbacks
def stop(self):
@maybe_coroutine
async def stop(self):
# Stop timers
if self.save_state_timer.running:
self.save_state_timer.stop()
@ -256,11 +267,11 @@ def stop(self):
self.prev_status_cleanup_loop.stop()
# Save state on shutdown
yield self.save_state()
await self.save_state()
self.session.pause()
result = yield self.save_resume_data(flush_disk_cache=True)
result = await self.save_resume_data(flush_disk_cache=True)
# Remove the temp_file to signify successfully saved state
if result and os.path.isfile(self.temp_file):
os.remove(self.temp_file)
@ -274,11 +285,6 @@ def update(self):
'Paused',
'Queued',
):
# If the global setting is set, but the per-torrent isn't...
# Just skip to the next torrent.
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
if not torrent.options['stop_at_ratio']:
continue
if (
torrent.get_ratio() >= torrent.options['stop_ratio']
and torrent.is_finished
@ -286,7 +292,7 @@ def update(self):
if torrent.options['remove_at_ratio']:
self.remove(torrent_id)
break
if not torrent.handle.status().paused:
if not torrent.status.paused:
torrent.pause()
def __getitem__(self, torrent_id):
@ -339,26 +345,28 @@ def get_torrent_info_from_file(self, filepath):
else:
return torrent_info
def prefetch_metadata(self, magnet, timeout):
"""Download the metadata for a magnet uri.
@maybe_coroutine
async def prefetch_metadata(self, magnet: str, timeout: int) -> Tuple[str, bytes]:
"""Download the metadata for a magnet URI.
Args:
magnet (str): A magnet uri to download the metadata for.
timeout (int): Number of seconds to wait before cancelling.
magnet: A magnet URI to download the metadata for.
timeout: Number of seconds to wait before canceling.
Returns:
Deferred: A tuple of (torrent_id (str), metadata (dict))
A tuple of (torrent_id, metadata)
"""
torrent_id = get_magnet_info(magnet)['info_hash']
if torrent_id in self.prefetching_metadata:
return self.prefetching_metadata[torrent_id].defer
d = Deferred()
self.prefetching_metadata[torrent_id].result_queue.append(d)
return await d
add_torrent_params = {}
add_torrent_params['save_path'] = gettempdir()
add_torrent_params['url'] = magnet.strip().encode('utf8')
add_torrent_params['flags'] = (
add_torrent_params = lt.parse_magnet_uri(magnet)
add_torrent_params.save_path = gettempdir()
add_torrent_params.flags = (
(
LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
@ -372,33 +380,29 @@ def prefetch_metadata(self, magnet, timeout):
d = Deferred()
# Cancel the defer if timeout reached.
defer_timeout = self.callLater(timeout, d.cancel)
d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout)
Prefetch = namedtuple('Prefetch', 'defer handle')
self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle)
return d
d.addTimeout(timeout, self.clock)
self.prefetching_metadata[torrent_id] = PrefetchQueueItem(d, [])
def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout):
# Cancel reactor.callLater.
try:
defer_timeout.cancel()
except error.AlreadyCalled:
pass
torrent_info = await d
except (defer.TimeoutError, defer.CancelledError):
log.debug(f'Prefetching metadata for {torrent_id} timed out or cancelled.')
metadata = b''
else:
log.debug('prefetch metadata received')
if VersionSplit(LT_VERSION) < VersionSplit('2.0.0.0'):
metadata = torrent_info.metadata()
else:
metadata = torrent_info.info_section()
log.debug('remove prefetch magnet from session')
try:
torrent_handle = self.prefetching_metadata.pop(torrent_id).handle
except KeyError:
pass
else:
self.session.remove_torrent(torrent_handle, 1)
result_queue = self.prefetching_metadata.pop(torrent_id).result_queue
self.session.remove_torrent(torrent_handle, 1)
result = torrent_id, b64encode(metadata)
metadata = None
if isinstance(torrent_info, lt.torrent_info):
log.debug('prefetch metadata received')
metadata = lt.bdecode(torrent_info.metadata())
return torrent_id, metadata
for d in result_queue:
d.callback(result)
return result
def _build_torrent_options(self, options):
"""Load default options and update if needed."""
@ -431,9 +435,10 @@ def _build_torrent_params(
elif magnet:
magnet_info = get_magnet_info(magnet)
if magnet_info:
add_torrent_params['url'] = magnet.strip().encode('utf8')
add_torrent_params['name'] = magnet_info['name']
add_torrent_params['trackers'] = list(magnet_info['trackers'])
torrent_id = magnet_info['info_hash']
add_torrent_params['info_hash'] = bytes(bytearray.fromhex(torrent_id))
else:
raise AddTorrentError(
'Unable to add magnet, invalid magnet info: %s' % magnet
@ -448,7 +453,7 @@ def _build_torrent_params(
raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
elif torrent_id in self.prefetching_metadata:
# Cancel and remove metadata fetching torrent.
self.prefetching_metadata[torrent_id].defer.cancel()
self.prefetching_metadata[torrent_id].alert_deferred.cancel()
# Check for renamed files and if so, rename them in the torrent_info before adding.
if options['mapped_files'] and torrent_info:
@ -509,7 +514,7 @@ def add(
save_state (bool, optional): If True save the session state after adding torrent, defaults to True.
filedump (str, optional): bencoded filedump of a torrent file.
filename (str, optional): The filename of the torrent file.
magnet (str, optional): The magnet uri.
magnet (str, optional): The magnet URI.
resume_data (lt.entry, optional): libtorrent fast resume data.
Returns:
@ -574,7 +579,7 @@ def add_async(
save_state (bool, optional): If True save the session state after adding torrent, defaults to True.
filedump (str, optional): bencoded filedump of a torrent file.
filename (str, optional): The filename of the torrent file.
magnet (str, optional): The magnet uri.
magnet (str, optional): The magnet URI.
resume_data (lt.entry, optional): libtorrent fast resume data.
Returns:
@ -642,7 +647,7 @@ def _add_torrent_obj(
# Resume AlertManager if paused for adding torrent to libtorrent.
component.resume('AlertManager')
# Store the orignal resume_data, in case of errors.
# Store the original resume_data, in case of errors.
if resume_data:
self.resume_data[torrent.torrent_id] = resume_data
@ -809,9 +814,9 @@ def open_state(self):
try:
with open(filepath, 'rb') as _file:
state = pickle.load(_file)
except (IOError, EOFError, pickle.UnpicklingError) as ex:
message = 'Unable to load {}: {}'.format(filepath, ex)
state = pickle.load(_file, encoding='utf8')
except (OSError, EOFError, pickle.UnpicklingError) as ex:
message = f'Unable to load {filepath}: {ex}'
log.error(message)
if not filepath.endswith('.bak'):
self.archive_state(message)
@ -1022,7 +1027,7 @@ def save_resume_data(self, torrent_ids=None, flush_disk_cache=False):
)
def on_torrent_resume_save(dummy_result, torrent_id):
"""Recieved torrent resume_data alert so remove from waiting list"""
"""Received torrent resume_data alert so remove from waiting list"""
self.waiting_on_resume_data.pop(torrent_id, None)
deferreds = []
@ -1067,7 +1072,7 @@ def load_resume_data_file(self):
try:
with open(_filepath, 'rb') as _file:
resume_data = lt.bdecode(_file.read())
except (IOError, EOFError, RuntimeError) as ex:
except (OSError, EOFError, RuntimeError) as ex:
if self.torrents:
log.warning('Unable to load %s: %s', _filepath, ex)
resume_data = None
@ -1240,7 +1245,7 @@ def on_set_max_download_speed_per_torrent(self, key, value):
def on_alert_add_torrent(self, alert):
"""Alert handler for libtorrent add_torrent_alert"""
if not alert.handle.is_valid():
log.warning('Torrent handle is invalid!')
log.warning('Torrent handle is invalid: %s', alert.error.message())
return
try:
@ -1351,10 +1356,8 @@ def on_alert_tracker_reply(self, alert):
torrent.set_tracker_status('Announce OK')
# Check for peer information from the tracker, if none then send a scrape request.
if (
alert.handle.status().num_complete == -1
or alert.handle.status().num_incomplete == -1
):
torrent.get_lt_status()
if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
torrent.scrape_tracker()
def on_alert_tracker_announce(self, alert):
@ -1389,7 +1392,18 @@ def on_alert_tracker_error(self, alert):
log.debug(
'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message
)
torrent.set_tracker_status('Error: ' + error_message)
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
# we will need to verify that at least one endpoint to the errored tracker is working
for tracker in torrent.handle.trackers():
if tracker['url'] == alert.url:
if any(
endpoint['last_error']['value'] == 0
for endpoint in tracker['endpoints']
):
torrent.set_tracker_status('Announce OK')
else:
torrent.set_tracker_status('Error: ' + error_message)
break
def on_alert_storage_moved(self, alert):
"""Alert handler for libtorrent storage_moved_alert"""
@ -1463,7 +1477,9 @@ def on_alert_save_resume_data(self, alert):
return
if torrent_id in self.torrents:
# libtorrent add_torrent expects bencoded resume_data.
self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
self.resume_data[torrent_id] = lt.bencode(
lt.write_resume_data(alert.params)
)
if torrent_id in self.waiting_on_resume_data:
self.waiting_on_resume_data[torrent_id].callback(None)
@ -1545,7 +1561,7 @@ def on_alert_metadata_received(self, alert):
# Try callback to prefetch_metadata method.
try:
d = self.prefetching_metadata[torrent_id].defer
d = self.prefetching_metadata[torrent_id].alert_deferred
except KeyError:
pass
else:
@ -1591,23 +1607,14 @@ def on_alert_state_update(self, alert):
except RuntimeError:
continue
if torrent_id in self.torrents:
self.torrents[torrent_id].update_status(t_status)
self.torrents[torrent_id].status = t_status
self.handle_torrents_status_callback(self.torrents_status_requests.pop())
def on_alert_external_ip(self, alert):
"""Alert handler for libtorrent external_ip_alert
Note:
The alert.message IPv4 address format is:
'external IP received: 0.0.0.0'
and IPv6 address format is:
'external IP received: 0:0:0:0:0:0:0:0'
"""
external_ip = decode_bytes(alert.message()).split(' ')[-1]
log.info('on_alert_external_ip: %s', external_ip)
component.get('EventManager').emit(ExternalIPEvent(external_ip))
"""Alert handler for libtorrent external_ip_alert"""
log.info('on_alert_external_ip: %s', alert.external_address)
component.get('EventManager').emit(ExternalIPEvent(alert.external_address))
def on_alert_performance(self, alert):
"""Alert handler for libtorrent performance_alert"""

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
#
@ -7,8 +6,10 @@
# See LICENSE for more details.
#
from __future__ import division, print_function, unicode_literals
import os
import stat
from OpenSSL import crypto
from OpenSSL.crypto import FILETYPE_PEM
from twisted.internet.ssl import (
AcceptableCiphers,
@ -18,6 +19,8 @@
TLSVersion,
)
import deluge.configmanager
# A TLS ciphers list.
# Sources for more information on TLS ciphers:
# - https://wiki.mozilla.org/Security/Server_Side_TLS
@ -77,3 +80,57 @@ def get_context_factory(cert_path, pkey_path):
ctx.set_options(SSL_OP_NO_RENEGOTIATION)
return cert_options
def check_ssl_keys():
"""
Check for SSL cert/key and create them if necessary
"""
ssl_dir = deluge.configmanager.get_config_dir('ssl')
if not os.path.exists(ssl_dir):
# The ssl folder doesn't exist so we need to create it
os.makedirs(ssl_dir)
generate_ssl_keys()
else:
for f in ('daemon.pkey', 'daemon.cert'):
if not os.path.exists(os.path.join(ssl_dir, f)):
generate_ssl_keys()
break
def generate_ssl_keys():
"""
This method generates a new SSL key/cert.
"""
digest = 'sha256'
# Generate key pair
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
# Generate cert request
req = crypto.X509Req()
subj = req.get_subject()
setattr(subj, 'CN', 'Deluge Daemon')
req.set_pubkey(pkey)
req.sign(pkey, digest)
# Generate certificate
cert = crypto.X509()
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, digest)
# Write out files
ssl_dir = deluge.configmanager.get_config_dir('ssl')
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
# Make the files only readable by this user
for f in ('daemon.pkey', 'daemon.cert'):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
#
@ -7,12 +6,13 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import inspect
import re
import warnings
from functools import wraps
from typing import Any, Callable, Coroutine, TypeVar
from twisted.internet import defer
def proxy(proxy_func):
@ -56,7 +56,7 @@ def funcname
if inspect.isfunction(args[0]):
return _overrides(stack, args[0])
else:
# One or more classes are specifed, so return a function that will be
# One or more classes are specified, so return a function that will be
# called with the real function as argument
def ret_func(func, **kwargs):
return _overrides(stack, func, explicit_base_classes=args)
@ -107,7 +107,7 @@ def get_class(cls_name):
for c in base_classes + check_classes:
classes[c] = get_class(c)
# Verify that the excplicit override class is one of base classes
# Verify that the explicit override class is one of base classes
if explicit_base_classes:
from itertools import product
@ -127,7 +127,7 @@ def get_class(cls_name):
% (
method.__name__,
cls,
'File: %s:%s' % (stack[1][1], stack[1][2]),
f'File: {stack[1][1]}:{stack[1][2]}',
)
)
@ -137,7 +137,7 @@ def get_class(cls_name):
% (
method.__name__,
check_classes,
'File: %s:%s' % (stack[1][1], stack[1][2]),
f'File: {stack[1][1]}:{stack[1][2]}',
)
)
return method
@ -146,7 +146,7 @@ def get_class(cls_name):
def deprecated(func):
"""This is a decorator which can be used to mark function as deprecated.
It will result in a warning being emmitted when the function is used.
It will result in a warning being emitted when the function is used.
"""
@ -154,7 +154,7 @@ def deprecated(func):
def depr_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # Turn off filter
warnings.warn(
'Call to deprecated function {}.'.format(func.__name__),
f'Call to deprecated function {func.__name__}.',
category=DeprecationWarning,
stacklevel=2,
)
@ -162,3 +162,57 @@ def depr_func(*args, **kwargs):
return func(*args, **kwargs)
return depr_func
class CoroutineDeferred(defer.Deferred):
"""Wraps a coroutine in a Deferred.
It will dynamically pass through the underlying coroutine without wrapping where apporpriate."""
def __init__(self, coro: Coroutine):
# Delay this import to make sure a reactor was installed first
from twisted.internet import reactor
super().__init__()
self.coro = coro
self.awaited = None
self.activate_deferred = reactor.callLater(0, self.activate)
def __await__(self):
if self.awaited in [None, True]:
self.awaited = True
return self.coro.__await__()
# Already in deferred mode
return super().__await__()
def activate(self):
"""If the result wasn't awaited before the next context switch, we turn it into a deferred."""
if self.awaited is None:
self.awaited = False
try:
d = defer.Deferred.fromCoroutine(self.coro)
except AttributeError:
# Fallback for Twisted <= 21.2 without fromCoroutine
d = defer.ensureDeferred(self.coro)
d.chainDeferred(self)
def addCallbacks(self, *args, **kwargs): # noqa: N802
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
self.activate()
return super().addCallbacks(*args, **kwargs)
_RetT = TypeVar('_RetT')
def maybe_coroutine(
f: Callable[..., Coroutine[Any, Any, _RetT]]
) -> 'Callable[..., defer.Deferred[_RetT]]':
"""Wraps a coroutine function to make it usable as a normal function that returns a Deferred."""
@wraps(f)
def wrapper(*args, **kwargs):
# Uncomment for quick testing to make sure CoroutineDeferred magic isn't at fault
# return defer.ensureDeferred(f(*args, **kwargs))
return CoroutineDeferred(f(*args, **kwargs))
return wrapper

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -9,18 +8,15 @@
#
from __future__ import unicode_literals
class DelugeError(Exception):
def __new__(cls, *args, **kwargs):
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs)
inst = super().__new__(cls, *args, **kwargs)
inst._args = args
inst._kwargs = kwargs
return inst
def __init__(self, message=None):
super(DelugeError, self).__init__(message)
super().__init__(message)
self.message = message
def __str__(self):
@ -45,12 +41,12 @@ class InvalidPathError(DelugeError):
class WrappedException(DelugeError):
def __init__(self, message, exception_type, traceback):
super(WrappedException, self).__init__(message)
super().__init__(message)
self.type = exception_type
self.traceback = traceback
def __str__(self):
return '%s\n%s' % (self.message, self.traceback)
return f'{self.message}\n{self.traceback}'
class _ClientSideRecreateError(DelugeError):
@ -64,7 +60,7 @@ def __init__(self, daemon_version):
'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)
super().__init__(message=msg)
class NotAuthorizedError(_ClientSideRecreateError):
@ -73,14 +69,14 @@ def __init__(self, current_level, required_level):
'current_level': current_level,
'required_level': required_level,
}
super(NotAuthorizedError, self).__init__(message=msg)
super().__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)
super().__init__(message)
self.username = username
@ -94,3 +90,7 @@ class AuthenticationRequired(_UsernameBasedPasstroughError):
class AuthManagerError(_UsernameBasedPasstroughError):
pass
class LibtorrentImportError(ImportError):
pass

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -14,10 +13,6 @@
and subsequently emitted to the clients.
"""
from __future__ import unicode_literals
import six
known_events = {}
@ -27,12 +22,12 @@ class DelugeEventMetaClass(type):
"""
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super(DelugeEventMetaClass, cls).__init__(name, bases, dct)
super().__init__(name, bases, dct)
if name != 'DelugeEvent':
known_events[name] = cls
class DelugeEvent(six.with_metaclass(DelugeEventMetaClass, object)):
class DelugeEvent(metaclass=DelugeEventMetaClass):
"""
The base class for all events.

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
@ -7,8 +6,6 @@
# See LICENSE for more details.
#
from __future__ import unicode_literals
import cgi
import logging
import os.path
@ -19,7 +16,7 @@
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.error import Error, PageRedirect
from twisted.web.http_headers import Headers
from twisted.web.iweb import IAgent
from zope.interface import implementer
@ -40,11 +37,11 @@ class CompressionDecoderProtocol(client._GzipProtocol):
"""A compression decoder protocol for CompressionDecoder."""
def __init__(self, protocol, response):
super(CompressionDecoderProtocol, self).__init__(protocol, response)
super().__init__(protocol, response)
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
class BodyHandler(HTTPClientParser, object):
class BodyHandler(HTTPClientParser):
"""An HTTP parser that saves the response to a file."""
def __init__(self, request, finished, length, agent, encoding=None):
@ -56,7 +53,7 @@ def __init__(self, request, finished, length, agent, encoding=None):
length (int): The length of the response.
agent (t.w.i.IAgent): The agent from which the request was sent.
"""
super(BodyHandler, self).__init__(request, finished)
super().__init__(request, finished)
self.agent = agent
self.finished = finished
self.total_length = length
@ -76,12 +73,12 @@ def connectionLost(self, reason): # NOQA: N802
with open(self.agent.filename, 'wb') as _file:
_file.write(self.data)
self.finished.callback(self.agent.filename)
self.state = u'DONE'
self.state = 'DONE'
HTTPClientParser.connectionLost(self, reason)
@implementer(IAgent)
class HTTPDownloaderAgent(object):
class HTTPDownloaderAgent:
"""A File Downloader Agent."""
def __init__(
@ -125,6 +122,9 @@ def request_callback(self, response):
location = response.headers.getRawHeaders(b'location')[0]
error = PageRedirect(response.code, location=location)
finished.errback(Failure(error))
elif response.code >= 400:
error = Error(response.code)
finished.errback(Failure(error))
else:
headers = response.headers
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
@ -146,14 +146,17 @@ def request_callback(self, response):
fileext = os.path.splitext(new_file_name)[1]
while os.path.isfile(new_file_name):
# Increment filename if already exists
new_file_name = '%s-%s%s' % (fileroot, count, fileext)
new_file_name = f'{fileroot}-{count}{fileext}'
count += 1
self.filename = new_file_name
cont_type = headers.getRawHeaders(b'content-type')[0].decode()
params = cgi.parse_header(cont_type)[1]
encoding = params.get('charset', None)
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
cont_type, params = cgi.parse_header(cont_type_header)
# Only re-ecode text content types.
encoding = None
if cont_type.startswith('text/'):
encoding = params.get('charset', None)
response.deliverBody(
BodyHandler(response.request, finished, body_length, self, encoding)
)

6178
deluge/i18n/af.po Normal file

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

@ -7,53 +7,53 @@ msgid ""
msgstr ""
"Project-Id-Version: deluge\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2019-06-06 11:53+0100\n"
"PO-Revision-Date: 2019-01-17 20:26+0000\n"
"POT-Creation-Date: 2019-11-12 14:55+0000\n"
"PO-Revision-Date: 2019-07-23 10:47+0000\n"
"Last-Translator: scootergrisen <scootergrisen@gmail.com>\n"
"Language-Team: Danish <da@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2019-06-06 11:12+0000\n"
"X-Generator: Launchpad (build 18978)\n"
"X-Launchpad-Export-Date: 2021-09-10 18:01+0000\n"
"X-Generator: Launchpad (build aca2013fd8cd2fea408d75f89f9bc012fbab307d)\n"
#: deluge/common.py:405
#: deluge/common.py:411
msgid "B"
msgstr "B"
#: deluge/common.py:406
#: deluge/common.py:412
msgid "KiB"
msgstr "KiB"
#: deluge/common.py:407
#: deluge/common.py:413
msgid "MiB"
msgstr "MiB"
#: deluge/common.py:408
#: deluge/common.py:414
msgid "GiB"
msgstr "GiB"
#: deluge/common.py:409
#: deluge/common.py:415
msgid "TiB"
msgstr "TiB"
#: deluge/common.py:410
#: deluge/common.py:416
msgid "K"
msgstr "K"
#: deluge/common.py:411
#: deluge/common.py:417
msgid "M"
msgstr "M"
#: deluge/common.py:412
#: deluge/common.py:418
msgid "G"
msgstr "G"
#: deluge/common.py:413
#: deluge/common.py:419
msgid "T"
msgstr "T"
#: deluge/common.py:509 deluge/ui/gtk3/statusbar.py:442
#: deluge/common.py:515 deluge/ui/gtk3/statusbar.py:442
#: deluge/ui/gtk3/statusbar.py:455 deluge/ui/gtk3/statusbar.py:464
#: deluge/ui/gtk3/statusbar.py:477 deluge/ui/gtk3/statusbar.py:484
#: deluge/ui/gtk3/statusbar.py:526 deluge/ui/gtk3/statusbar.py:542
@ -64,7 +64,7 @@ msgstr "T"
msgid "K/s"
msgstr "K/s"
#: deluge/common.py:509 deluge/ui/gtk3/menubar.py:449
#: deluge/common.py:515 deluge/ui/gtk3/menubar.py:449
#: deluge/ui/gtk3/menubar.py:455
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:80
#: deluge/ui/console/widgets/statusbars.py:104
@ -78,27 +78,27 @@ msgstr "K/s"
msgid "KiB/s"
msgstr "KiB/s"
#: deluge/common.py:515
#: deluge/common.py:521
msgid "M/s"
msgstr "M/s"
#: deluge/common.py:515
#: deluge/common.py:521
msgid "MiB/s"
msgstr "MiB/s"
#: deluge/common.py:521
#: deluge/common.py:527
msgid "G/s"
msgstr "G/s"
#: deluge/common.py:521
#: deluge/common.py:527
msgid "GiB/s"
msgstr "GiB/s"
#: deluge/common.py:527
#: deluge/common.py:533
msgid "T/s"
msgstr "T/s"
#: deluge/common.py:527
#: deluge/common.py:533
msgid "TiB/s"
msgstr "TiB/s"
@ -191,7 +191,7 @@ msgstr ""
msgid "Config keys to be unmodified by `set_config` RPC"
msgstr ""
#: deluge/ui/common.py:37 deluge/ui/gtk3/filtertreeview.py:135
#: deluge/ui/common.py:37 deluge/ui/gtk3/filtertreeview.py:130
#: deluge/ui/web/js/deluge-all/UI.js:18
msgid "All"
msgstr "Alle"
@ -232,7 +232,7 @@ msgid "Queued"
msgstr "Sat i kø"
#: deluge/ui/common.py:45 deluge/ui/common.py:122
#: deluge/ui/gtk3/statusbar.py:396 deluge/ui/gtk3/filtertreeview.py:136
#: deluge/ui/gtk3/statusbar.py:396 deluge/ui/gtk3/filtertreeview.py:131
#: deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py:330
#: deluge/ui/web/js/deluge-all/AddConnectionWindow.js:94
#: deluge/ui/web/js/deluge-all/EditConnectionWindow.js:114
@ -241,7 +241,9 @@ msgstr "Sat i kø"
#: deluge/ui/web/js/deluge-all/ConnectionManager.js:417
#: deluge/ui/web/js/deluge-all/UI.js:27
#: deluge/ui/web/js/deluge-all/details/StatusTab.js:121
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:301
#: deluge/ui/web/js/deluge-all/add/UrlWindow.js:98
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:291
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:316
msgid "Error"
msgstr "Fejl"
@ -265,7 +267,7 @@ msgid "State"
msgstr "Status"
#: deluge/ui/common.py:54 deluge/ui/gtk3/createtorrentdialog.py:72
#: deluge/ui/gtk3/addtorrentdialog.py:118 deluge/ui/gtk3/files_tab.py:113
#: deluge/ui/gtk3/addtorrentdialog.py:123 deluge/ui/gtk3/files_tab.py:113
#: deluge/ui/gtk3/torrentview.py:283
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:174
#: deluge/ui/console/modes/preferences/preference_panes.py:738
@ -449,7 +451,7 @@ msgstr "Sti til flyt fuldførte"
msgid "Move On Completed Path"
msgstr ""
#: deluge/ui/common.py:115 deluge/ui/gtk3/filtertreeview.py:140
#: deluge/ui/common.py:115 deluge/ui/gtk3/filtertreeview.py:135
#: deluge/ui/gtk3/torrentview.py:416
#: deluge/plugins/AutoAdd/deluge_autoadd/gtkui.py:499
#: deluge/ui/web/js/deluge-all/FilterPanel.js:32
@ -1621,7 +1623,7 @@ msgid "Daemon not running"
msgstr "Dæmon kører ikke"
#: deluge/ui/gtk3/createtorrentdialog.py:62
#: deluge/ui/gtk3/addtorrentdialog.py:105 deluge/ui/gtk3/files_tab.py:92
#: deluge/ui/gtk3/addtorrentdialog.py:110 deluge/ui/gtk3/files_tab.py:92
#: deluge/ui/web/js/deluge-all/details/FilesTab.js:18
#: deluge/ui/web/js/deluge-all/add/FilesTab.js:28
msgid "Filename"
@ -1640,7 +1642,7 @@ msgstr "Vælg en fil"
#: deluge/ui/gtk3/createtorrentdialog.py:132
#: deluge/ui/gtk3/createtorrentdialog.py:169
#: deluge/ui/gtk3/createtorrentdialog.py:258
#: deluge/ui/gtk3/addtorrentdialog.py:690 deluge/ui/gtk3/dialogs.py:203
#: deluge/ui/gtk3/addtorrentdialog.py:698 deluge/ui/gtk3/dialogs.py:203
#: deluge/ui/gtk3/dialogs.py:261 deluge/ui/gtk3/dialogs.py:273
#: deluge/ui/gtk3/dialogs.py:364 deluge/ui/gtk3/dialogs.py:427
#: deluge/ui/gtk3/preferences.py:1158
@ -1664,7 +1666,7 @@ msgstr ""
#: deluge/ui/gtk3/createtorrentdialog.py:134
#: deluge/ui/gtk3/createtorrentdialog.py:171
#: deluge/ui/gtk3/addtorrentdialog.py:692 deluge/ui/gtk3/preferences.py:1160
#: deluge/ui/gtk3/addtorrentdialog.py:700 deluge/ui/gtk3/preferences.py:1160
msgid "_Open"
msgstr ""
@ -1685,29 +1687,29 @@ msgid "_Save"
msgstr ""
#: deluge/ui/gtk3/createtorrentdialog.py:271
#: deluge/ui/gtk3/addtorrentdialog.py:704
#: deluge/ui/gtk3/addtorrentdialog.py:712
msgid "Torrent files"
msgstr "Torrent-filer"
#: deluge/ui/gtk3/createtorrentdialog.py:275
#: deluge/ui/gtk3/addtorrentdialog.py:708
#: deluge/ui/gtk3/addtorrentdialog.py:716
msgid "All files"
msgstr "Alle filer"
#: deluge/ui/gtk3/mainwindow.py:185
#: deluge/ui/gtk3/mainwindow.py:192
msgid "Enter your password to show Deluge..."
msgstr "Indtast din adgangskode for at vise Deluge..."
#: deluge/ui/gtk3/mainwindow.py:244
#: deluge/ui/gtk3/mainwindow.py:251
msgid "Enter your password to Quit Deluge..."
msgstr "Indtast din adgangskode for at afslutte Deluge..."
#: deluge/ui/gtk3/mainwindow.py:336
#: deluge/ui/gtk3/mainwindow.py:343
#, python-brace-format
msgid "D: {download_rate} U: {upload_rate} - Deluge"
msgstr ""
#: deluge/ui/gtk3/mainwindow.py:350 deluge/ui/gtk3/aboutdialog.py:26
#: deluge/ui/gtk3/mainwindow.py:357 deluge/ui/gtk3/aboutdialog.py:26
#: deluge/ui/gtk3/aboutdialog.py:27 deluge/ui/gtk3/systemtray.py:96
#: deluge/ui/gtk3/systemtray.py:184 deluge/ui/gtk3/systemtray.py:244
#: deluge/ui/data/share/applications/deluge.desktop.in.h:1
@ -1716,6 +1718,16 @@ msgstr ""
msgid "Deluge"
msgstr "Deluge"
#: deluge/ui/gtk3/path_combo_chooser.py:393
#: deluge/ui/gtk3/glade/path_combo_chooser.ui.h:20
msgid "Edit path"
msgstr ""
#: deluge/ui/gtk3/path_combo_chooser.py:395
#: deluge/ui/gtk3/glade/path_combo_chooser.ui.h:21
msgid "Remove path"
msgstr ""
#: deluge/ui/gtk3/options_tab.py:136
msgid "_Apply to selected"
msgstr ""
@ -1806,59 +1818,59 @@ msgstr "Server:"
msgid "libtorrent:"
msgstr "libtorrent:"
#: deluge/ui/gtk3/addtorrentdialog.py:97 deluge/ui/gtk3/queuedtorrents.py:51
#: deluge/ui/gtk3/addtorrentdialog.py:102 deluge/ui/gtk3/queuedtorrents.py:51
msgid "Torrent"
msgstr "Torrent"
#: deluge/ui/gtk3/addtorrentdialog.py:224
#: deluge/ui/gtk3/addtorrentdialog.py:232
#, python-format
msgid "Add Torrents (%d)"
msgstr ""
#: deluge/ui/gtk3/addtorrentdialog.py:230
#: deluge/ui/gtk3/addtorrentdialog.py:238
msgid "Duplicate torrent(s)"
msgstr ""
#: deluge/ui/gtk3/addtorrentdialog.py:232
#: deluge/ui/gtk3/addtorrentdialog.py:240
#, python-format
msgid ""
"You cannot add the same torrent twice. %d torrents were already added."
msgstr ""
#: deluge/ui/gtk3/addtorrentdialog.py:247
#: deluge/ui/gtk3/addtorrentdialog.py:255
msgid "Invalid File"
msgstr "Ugyldig fil"
#: deluge/ui/gtk3/addtorrentdialog.py:282
#: deluge/ui/gtk3/addtorrentdialog.py:290
#: deluge/ui/gtk3/glade/add_torrent_dialog.ui.h:8
msgid "Please wait for files..."
msgstr ""
#: deluge/ui/gtk3/addtorrentdialog.py:288
#: deluge/ui/gtk3/addtorrentdialog.py:296
msgid "Unable to download files for this magnet"
msgstr ""
#: deluge/ui/gtk3/addtorrentdialog.py:686
#: deluge/ui/gtk3/addtorrentdialog.py:694
msgid "Choose a .torrent file"
msgstr "Vælg en .torrent-fil"
#: deluge/ui/gtk3/addtorrentdialog.py:769
#: deluge/ui/gtk3/addtorrentdialog.py:777
msgid "Invalid URL"
msgstr "Ugyldig URL"
#: deluge/ui/gtk3/addtorrentdialog.py:770
#: deluge/ui/gtk3/addtorrentdialog.py:778
msgid "is not a valid URL."
msgstr "er ikke et gyldigt URL."
#: deluge/ui/gtk3/addtorrentdialog.py:776
#: deluge/ui/gtk3/addtorrentdialog.py:784
msgid "Downloading..."
msgstr "Downloader..."
#: deluge/ui/gtk3/addtorrentdialog.py:811
#: deluge/ui/gtk3/addtorrentdialog.py:819
msgid "Download Failed"
msgstr "Download mislykkedes"
#: deluge/ui/gtk3/addtorrentdialog.py:812
#: deluge/ui/gtk3/addtorrentdialog.py:820
msgid "Failed to download:"
msgstr "Fejlslagne download(s):"
@ -2153,29 +2165,29 @@ msgstr "Ned"
msgid "Up"
msgstr "Op"
#: deluge/ui/gtk3/gtkui.py:318
#: deluge/ui/gtk3/gtkui.py:313
msgid ""
"A Deluge daemon (deluged) is already running.\n"
"To use Standalone mode, stop local daemon and restart Deluge."
msgstr ""
#: deluge/ui/gtk3/gtkui.py:324
#: deluge/ui/gtk3/gtkui.py:319
msgid ""
"Only Thin Client mode is available because libtorrent is not installed.\n"
"To use Standalone mode, please install libtorrent package."
msgstr ""
#: deluge/ui/gtk3/gtkui.py:330 deluge/ui/gtk3/gtkui.py:336
#: deluge/ui/gtk3/gtkui.py:325 deluge/ui/gtk3/gtkui.py:331
msgid ""
"Only Thin Client mode is available due to unknown Import Error.\n"
"To use Standalone mode, please see logs for error details."
msgstr ""
#: deluge/ui/gtk3/gtkui.py:354
#: deluge/ui/gtk3/gtkui.py:349
msgid "Continue in Thin Client mode?"
msgstr ""
#: deluge/ui/gtk3/gtkui.py:355
#: deluge/ui/gtk3/gtkui.py:350
msgid "Change User Interface Mode"
msgstr ""
@ -2214,7 +2226,7 @@ msgstr "Version"
#: deluge/ui/gtk3/connectionmanager.py:219
#: deluge/ui/gtk3/glade/connection_manager.ui.h:8
msgid "_Start Daemon"
msgstr ""
msgstr "_Start dæmon"
#: deluge/ui/gtk3/connectionmanager.py:250
msgid "_Stop Daemon"
@ -2297,6 +2309,15 @@ msgstr ""
msgid "You must now restart the deluge UI for the changes to take effect."
msgstr ""
#: deluge/ui/gtk3/preferences.py:940
msgid "Thinclient"
msgstr ""
#: deluge/ui/gtk3/preferences.py:940
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:18
msgid "Standalone"
msgstr ""
#: deluge/ui/gtk3/preferences.py:942
msgid "Switching Deluge Client Mode..."
msgstr ""
@ -2365,39 +2386,39 @@ msgstr ""
msgid "An error occurred while removing account"
msgstr ""
#: deluge/ui/gtk3/filtertreeview.py:127
#: deluge/ui/gtk3/filtertreeview.py:122
#: deluge/ui/web/js/deluge-all/FilterPanel.js:28
msgid "States"
msgstr "Tilstande"
#: deluge/ui/gtk3/filtertreeview.py:133
#: deluge/ui/gtk3/filtertreeview.py:128
#: deluge/ui/gtk3/glade/create_torrent_dialog.ui.h:23
#: deluge/plugins/Label/deluge_label/data/label_options.ui.h:21
#: deluge/ui/web/js/deluge-all/FilterPanel.js:30
msgid "Trackers"
msgstr "Trackere"
#: deluge/ui/gtk3/filtertreeview.py:137 deluge/ui/gtk3/filtertreeview.py:143
#: deluge/ui/gtk3/filtertreeview.py:132 deluge/ui/gtk3/filtertreeview.py:138
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:7
#: deluge/ui/web/js/deluge-all/preferences/ProxyField.js:33
msgid "None"
msgstr "Ingen"
#: deluge/ui/gtk3/filtertreeview.py:142
#: deluge/ui/gtk3/filtertreeview.py:137
msgid "Admin"
msgstr "Admin"
#: deluge/ui/gtk3/filtertreeview.py:164
#: deluge/ui/gtk3/filtertreeview.py:159
#: deluge/ui/web/js/deluge-all/FilterPanel.js:34
msgid "Labels"
msgstr "Etiketter"
#: deluge/ui/gtk3/filtertreeview.py:209
#: deluge/ui/gtk3/filtertreeview.py:204
#: deluge/plugins/Label/deluge_label/gtkui/submenu.py:28
msgid "No Label"
msgstr "Ingen Etiket"
#: deluge/ui/gtk3/filtertreeview.py:211
#: deluge/ui/gtk3/filtertreeview.py:206
msgid "No Owner"
msgstr ""
@ -2536,7 +2557,7 @@ msgstr "Afslut og _stop dæmon"
#: deluge/ui/gtk3/glade/main_window.ui.h:5
#: deluge/ui/gtk3/glade/tray_menu.ui.h:8
msgid "_Quit"
msgstr ""
msgstr "_Afslut"
#: deluge/ui/gtk3/glade/main_window.ui.h:6
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:192
@ -2794,10 +2815,6 @@ msgstr ""
msgid "I2P"
msgstr ""
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:18
msgid "Standalone"
msgstr ""
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:19
msgid "The standalone self-contained application"
msgstr ""
@ -2912,7 +2929,7 @@ msgid "System Default"
msgstr ""
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:48
msgid "<b>Languge</b>"
msgid "<b>Language</b>"
msgstr ""
#: deluge/ui/gtk3/glade/preferences_dialog.ui.h:49
@ -3740,14 +3757,6 @@ msgstr ""
msgid "Ctrl+D"
msgstr ""
#: deluge/ui/gtk3/glade/path_combo_chooser.ui.h:20
msgid "Edit path"
msgstr ""
#: deluge/ui/gtk3/glade/path_combo_chooser.ui.h:21
msgid "Remove path"
msgstr ""
#: deluge/ui/gtk3/glade/path_combo_chooser.ui.h:22
msgid "Toggle hidden files"
msgstr ""
@ -5528,36 +5537,36 @@ msgstr "Pop op-notifikation er ikke slået til."
msgid "libnotify is not installed"
msgstr ""
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:183
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:185
msgid "Failed to popup notification"
msgstr ""
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:186
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:188
msgid "Notification popup shown"
msgstr "Notifikations-pop op vist"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:190
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:192
msgid "Sound notification not enabled"
msgstr "Lydnotifikation ikke slået til"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:192
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:194
msgid "pygame is not installed"
msgstr "pygame er ikke installeret"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:204
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:206
#, python-format
msgid "Sound notification failed %s"
msgstr "Lydnotifikation fejlede %s"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:208
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:210
msgid "Sound notification Success"
msgstr "Lydpåmindelse lykkedes"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:232
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:234
msgid "Finished Torrent"
msgstr "Afsluttet torrent"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:236
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:238
#, python-format
msgid ""
"The torrent \"%(name)s\" including %(num_files)i file(s) has finished "
@ -5566,12 +5575,12 @@ msgstr ""
"Torrenten \"%(name)s\" inklusiv %(num_files)i fil(er) er færdige med at "
"downloade."
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:285
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:315
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:287
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:317
msgid "Notifications"
msgstr "Notifikationer"
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:661
#: deluge/plugins/Notifications/deluge_notifications/gtkui.py:663
msgid "Choose Sound File"
msgstr "Vælg lydfil"
@ -6173,6 +6182,10 @@ msgstr "Adresse"
msgid "Cookies"
msgstr "Cookies"
#: deluge/ui/web/js/deluge-all/add/UrlWindow.js:99
msgid "Failed to download torrent"
msgstr ""
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:133
msgid "File"
msgstr "Fil"
@ -6181,11 +6194,15 @@ msgstr "Fil"
msgid "Infohash"
msgstr "Infohash"
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:259
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:260
msgid "Uploading your torrent..."
msgstr "Uploader din torrent..."
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:302
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:292
msgid "Failed to upload torrent"
msgstr ""
#: deluge/ui/web/js/deluge-all/add/AddWindow.js:317
msgid "Not a valid torrent"
msgstr "Ikke en gyldig torrent"
@ -6220,171 +6237,3 @@ msgstr ""
#: deluge/ui/web/render/tab_status.html:26
msgid "Date Added:"
msgstr "Dato tilføjet:"
#~ msgid "pynotify is not installed"
#~ msgstr "pynotify er ikke installeret"
#~ msgid "pynotify failed to show notification"
#~ msgstr "pynotify kunne ikke vise notifikation"
#~ msgid "<b><i><big>Notifications</big></i></b>"
#~ msgstr "<b><i><big>Notifikationer</big></i></b>"
#~ msgid "_Normal Priority"
#~ msgstr "_Normal prioritet"
#~ msgid "_High Priority"
#~ msgstr "_Høj prioritet"
#~ msgid "Associate Magnet links with Deluge"
#~ msgstr "Associer Magnet-links med Deluge"
#~ msgid "Bulgarian"
#~ msgstr "Bulgarsk"
#~ msgid "Arabic"
#~ msgstr "Arabisk"
#~ msgid "German"
#~ msgstr "Tysk"
#~ msgid "Danish"
#~ msgstr "Dansk"
#~ msgid "Bosnian"
#~ msgstr "Bosnisk"
#~ msgid "Czech"
#~ msgstr "Tjekkisk"
#~ msgid "Belarusian"
#~ msgstr "Hviderussisk"
#~ msgid "Bengali"
#~ msgstr "Bengalsk"
#~ msgid "Greek"
#~ msgstr "Græsk"
#~ msgid "English (Australia)"
#~ msgstr "Engelsk (Australien)"
#~ msgid "English (Canada)"
#~ msgstr "Engelsk (Canada)"
#~ msgid "English"
#~ msgstr "Engelsk"
#~ msgid "Spanish"
#~ msgstr "Spansk"
#~ msgid "English (United Kingdom)"
#~ msgstr "Engelsk (Storbritannien)"
#~ msgid "Esperanto"
#~ msgstr "Esperanto"
#~ msgid "Afrikaans"
#~ msgstr "Afrikaans"
#~ msgid "Irish"
#~ msgstr "Irsk"
#~ msgid "French"
#~ msgstr "Fransk"
#~ msgid "Finnish"
#~ msgstr "Finsk"
#~ msgid "Persian"
#~ msgstr "Persisk"
#~ msgid "Croatian"
#~ msgstr "Kroatisk"
#~ msgid "Indonesian"
#~ msgstr "Indonesisk"
#~ msgid "Icelandic"
#~ msgstr "Islandsk"
#~ msgid "Italian"
#~ msgstr "Italiensk"
#~ msgid "Interlingua"
#~ msgstr "Interlingua"
#~ msgid "Japanese"
#~ msgstr "Japansk"
#~ msgid "Macedonian"
#~ msgstr "Makedonsk"
#~ msgid "Korean"
#~ msgstr "Koreansk"
#~ msgid "Latin"
#~ msgstr "Latinsk"
#~ msgid "Kurdish"
#~ msgstr "Kurdisk"
#~ msgid "Mongolian"
#~ msgstr "Mongolsk"
#~ msgid "Polish"
#~ msgstr "Polsk"
#~ msgid "Burmese"
#~ msgstr "Burmesisk"
#~ msgid "Slovenian"
#~ msgstr "Slovensk"
#~ msgid "Slovak"
#~ msgstr "Slovakisk"
#~ msgid "Russian"
#~ msgstr "Russisk"
#~ msgid "Portuguese"
#~ msgstr "Portugisisk"
#~ msgid "Serbian"
#~ msgstr "Serbisk"
#~ msgid "Albanian"
#~ msgstr "Albansk"
#~ msgid "Traditional Chinese"
#~ msgstr "Kinesisk (traditionel)"
#~ msgid "Simplified Chinese"
#~ msgstr "Kinesisk (forenklet)"
#~ msgid "Vietnamese"
#~ msgstr "Vietnamesisk"
#~ msgid "Chinese (Hong Kong)"
#~ msgstr "Kinesisk (Hong Kong)"
#~ msgid "Chinese (Simplified)"
#~ msgstr "Kinesisk (forenklet)"
#~ msgid "Chinese (Taiwan)"
#~ msgstr "Kinesisk (Taiwan)"
#~ msgid "Ignore"
#~ msgstr "Ignorer"
#~ msgid "Estonian"
#~ msgstr "Estisk"
#~ msgid "Hebrew"
#~ msgstr "Hebraisk"
#~ msgid "Hungarian"
#~ msgstr "Ungarsk"
#~ msgid "Dutch"
#~ msgstr "Hollandsk"

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

6164
deluge/i18n/fo.po Normal file

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

6164
deluge/i18n/ga.po Normal file

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

6172
deluge/i18n/km.po Normal file

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

6164
deluge/i18n/ky.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is public domain.
#
from __future__ import unicode_literals
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'

6164
deluge/i18n/lb.po Normal file

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

6164
deluge/i18n/ml.po Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

6172
deluge/i18n/nap.po Normal file

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

6180
deluge/i18n/nn.po Normal file

File diff suppressed because it is too large Load diff

6171
deluge/i18n/oc.po Normal file

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

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