Merging upstream version 2.1.1 (Closes: #1026291).
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
d90f010c88
commit
111de75aff
522 changed files with 304108 additions and 161556 deletions
17
AUTHORS
17
AUTHORS
|
@ -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>
|
||||
|
|
181
CHANGELOG.md
181
CHANGELOG.md
|
@ -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
|
||||
|
|
21
DEPENDS.md
21
DEPENDS.md
|
@ -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/
|
||||
|
|
|
@ -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
144
PKG-INFO
|
@ -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
|
||||
|
|
26
README.md
26
README.md
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.0.3
|
||||
2.1.1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}'
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
331
deluge/common.py
331
deluge/common.py
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
219
deluge/config.py
219
deluge/config.py
|
@ -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()
|
||||
|
|
|
@ -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
192
deluge/conftest.py
Normal 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
|
|
@ -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})
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
6178
deluge/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
1432
deluge/i18n/ar.po
1432
deluge/i18n/ar.po
File diff suppressed because it is too large
Load diff
8048
deluge/i18n/ast.po
8048
deluge/i18n/ast.po
File diff suppressed because it is too large
Load diff
7272
deluge/i18n/be.po
7272
deluge/i18n/be.po
File diff suppressed because it is too large
Load diff
8134
deluge/i18n/bg.po
8134
deluge/i18n/bg.po
File diff suppressed because it is too large
Load diff
6763
deluge/i18n/bn.po
6763
deluge/i18n/bn.po
File diff suppressed because it is too large
Load diff
6820
deluge/i18n/bs.po
6820
deluge/i18n/bs.po
File diff suppressed because it is too large
Load diff
2281
deluge/i18n/ca.po
2281
deluge/i18n/ca.po
File diff suppressed because it is too large
Load diff
1615
deluge/i18n/cs.po
1615
deluge/i18n/cs.po
File diff suppressed because it is too large
Load diff
6812
deluge/i18n/cy.po
6812
deluge/i18n/cy.po
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
|
|
8177
deluge/i18n/de.po
8177
deluge/i18n/de.po
File diff suppressed because it is too large
Load diff
8244
deluge/i18n/el.po
8244
deluge/i18n/el.po
File diff suppressed because it is too large
Load diff
8240
deluge/i18n/en_AU.po
8240
deluge/i18n/en_AU.po
File diff suppressed because it is too large
Load diff
8268
deluge/i18n/en_CA.po
8268
deluge/i18n/en_CA.po
File diff suppressed because it is too large
Load diff
1593
deluge/i18n/en_GB.po
1593
deluge/i18n/en_GB.po
File diff suppressed because it is too large
Load diff
6757
deluge/i18n/eo.po
6757
deluge/i18n/eo.po
File diff suppressed because it is too large
Load diff
1239
deluge/i18n/es.po
1239
deluge/i18n/es.po
File diff suppressed because it is too large
Load diff
8225
deluge/i18n/et.po
8225
deluge/i18n/et.po
File diff suppressed because it is too large
Load diff
7470
deluge/i18n/eu.po
7470
deluge/i18n/eu.po
File diff suppressed because it is too large
Load diff
6981
deluge/i18n/fa.po
6981
deluge/i18n/fa.po
File diff suppressed because it is too large
Load diff
2183
deluge/i18n/fi.po
2183
deluge/i18n/fi.po
File diff suppressed because it is too large
Load diff
6164
deluge/i18n/fo.po
Normal file
6164
deluge/i18n/fo.po
Normal file
File diff suppressed because it is too large
Load diff
2598
deluge/i18n/fr.po
2598
deluge/i18n/fr.po
File diff suppressed because it is too large
Load diff
7674
deluge/i18n/fy.po
7674
deluge/i18n/fy.po
File diff suppressed because it is too large
Load diff
6164
deluge/i18n/ga.po
Normal file
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
1407
deluge/i18n/he.po
1407
deluge/i18n/he.po
File diff suppressed because it is too large
Load diff
7935
deluge/i18n/hi.po
7935
deluge/i18n/hi.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1568
deluge/i18n/hu.po
1568
deluge/i18n/hu.po
File diff suppressed because it is too large
Load diff
7078
deluge/i18n/id.po
7078
deluge/i18n/id.po
File diff suppressed because it is too large
Load diff
8160
deluge/i18n/is.po
8160
deluge/i18n/is.po
File diff suppressed because it is too large
Load diff
1651
deluge/i18n/it.po
1651
deluge/i18n/it.po
File diff suppressed because it is too large
Load diff
6798
deluge/i18n/iu.po
6798
deluge/i18n/iu.po
File diff suppressed because it is too large
Load diff
8009
deluge/i18n/ja.po
8009
deluge/i18n/ja.po
File diff suppressed because it is too large
Load diff
7555
deluge/i18n/ka.po
7555
deluge/i18n/ka.po
File diff suppressed because it is too large
Load diff
8077
deluge/i18n/kk.po
8077
deluge/i18n/kk.po
File diff suppressed because it is too large
Load diff
6172
deluge/i18n/km.po
Normal file
6172
deluge/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
6962
deluge/i18n/kn.po
6962
deluge/i18n/kn.po
File diff suppressed because it is too large
Load diff
8147
deluge/i18n/ko.po
8147
deluge/i18n/ko.po
File diff suppressed because it is too large
Load diff
6774
deluge/i18n/ku.po
6774
deluge/i18n/ku.po
File diff suppressed because it is too large
Load diff
6164
deluge/i18n/ky.po
Normal file
6164
deluge/i18n/ky.po
Normal file
File diff suppressed because it is too large
Load diff
6755
deluge/i18n/la.po
6755
deluge/i18n/la.po
File diff suppressed because it is too large
Load diff
|
@ -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
6164
deluge/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load diff
8222
deluge/i18n/lt.po
8222
deluge/i18n/lt.po
File diff suppressed because it is too large
Load diff
8283
deluge/i18n/lv.po
8283
deluge/i18n/lv.po
File diff suppressed because it is too large
Load diff
7457
deluge/i18n/mk.po
7457
deluge/i18n/mk.po
File diff suppressed because it is too large
Load diff
6164
deluge/i18n/ml.po
Normal file
6164
deluge/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
8371
deluge/i18n/ms.po
8371
deluge/i18n/ms.po
File diff suppressed because it is too large
Load diff
6172
deluge/i18n/nap.po
Normal file
6172
deluge/i18n/nap.po
Normal file
File diff suppressed because it is too large
Load diff
8225
deluge/i18n/nb.po
8225
deluge/i18n/nb.po
File diff suppressed because it is too large
Load diff
6755
deluge/i18n/nds.po
6755
deluge/i18n/nds.po
File diff suppressed because it is too large
Load diff
8170
deluge/i18n/nl.po
8170
deluge/i18n/nl.po
File diff suppressed because it is too large
Load diff
6180
deluge/i18n/nn.po
Normal file
6180
deluge/i18n/nn.po
Normal file
File diff suppressed because it is too large
Load diff
6171
deluge/i18n/oc.po
Normal file
6171
deluge/i18n/oc.po
Normal file
File diff suppressed because it is too large
Load diff
8294
deluge/i18n/pl.po
8294
deluge/i18n/pl.po
File diff suppressed because it is too large
Load diff
6743
deluge/i18n/pms.po
6743
deluge/i18n/pms.po
File diff suppressed because it is too large
Load diff
1666
deluge/i18n/pt.po
1666
deluge/i18n/pt.po
File diff suppressed because it is too large
Load diff
1695
deluge/i18n/pt_BR.po
1695
deluge/i18n/pt_BR.po
File diff suppressed because it is too large
Load diff
8180
deluge/i18n/ro.po
8180
deluge/i18n/ro.po
File diff suppressed because it is too large
Load diff
1709
deluge/i18n/ru.po
1709
deluge/i18n/ru.po
File diff suppressed because it is too large
Load diff
7501
deluge/i18n/si.po
7501
deluge/i18n/si.po
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
Loading…
Add table
Add a link
Reference in a new issue