Compare commits
269 Commits
deluge-1.3
...
archive/1.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e050905b29 | ||
![]() |
6c3442e7e7 | ||
![]() |
993abbc6a6 | ||
![]() |
a2fcebe15c | ||
![]() |
b8e5ebe822 | ||
![]() |
e33a8fbea4 | ||
![]() |
bcc7a74725 | ||
![]() |
ffb8d9f8c3 | ||
![]() |
396417bcd0 | ||
![]() |
b13da8a42a | ||
![]() |
415979e2f7 | ||
![]() |
5f0694deb2 | ||
![]() |
6d14be18b0 | ||
![]() |
65fac156eb | ||
![]() |
956f2ad574 | ||
![]() |
275c93657f | ||
![]() |
38d7b7cdfd | ||
![]() |
7661127b9d | ||
![]() |
a6e8ac8725 | ||
![]() |
d91584b700 | ||
![]() |
5427cbb73a | ||
![]() |
d977915f32 | ||
![]() |
a86b6f0f8f | ||
![]() |
3dfe6af1ee | ||
![]() |
1f315a9ef0 | ||
![]() |
08c03d7678 | ||
![]() |
dd08cb29e5 | ||
![]() |
909176e9aa | ||
![]() |
85eeadcfca | ||
![]() |
f870741d9d | ||
![]() |
cc69c9c85b | ||
![]() |
41acade01a | ||
![]() |
9bec5142c7 | ||
![]() |
0f1f62ec62 | ||
![]() |
318ab17986 | ||
![]() |
25150f13af | ||
![]() |
7cde3efb94 | ||
![]() |
7f01dc909e | ||
![]() |
10ebf9b0b0 | ||
![]() |
3ba5443c76 | ||
![]() |
c39f00fa0b | ||
![]() |
3962c41a55 | ||
![]() |
42ba9086d0 | ||
![]() |
2d4dec669e | ||
![]() |
bcf0fe4a61 | ||
![]() |
1dc4c465c7 | ||
![]() |
b52de1549e | ||
![]() |
8a3f15e5c0 | ||
![]() |
8565eccb3d | ||
![]() |
30eaf775c2 | ||
![]() |
ffb1316f09 | ||
![]() |
bd80ad62a0 | ||
![]() |
78851becf2 | ||
![]() |
af76abb038 | ||
![]() |
bf01b53bda | ||
![]() |
8a48ec0126 | ||
![]() |
c3a02e5291 | ||
![]() |
3c1995476d | ||
![]() |
48cedf635f | ||
![]() |
0b4627be8a | ||
![]() |
739537f860 | ||
![]() |
df88c82265 | ||
![]() |
5394ac5604 | ||
![]() |
f739269dfd | ||
![]() |
f57ee74ee2 | ||
![]() |
798f5e2deb | ||
![]() |
a7fe4d4510 | ||
![]() |
6c73105a73 | ||
![]() |
e66be42c81 | ||
![]() |
2263463114 | ||
![]() |
454c7be364 | ||
![]() |
85fdacc0e7 | ||
![]() |
869dbab459 | ||
![]() |
852b51f224 | ||
![]() |
492ad07965 | ||
![]() |
904a51835b | ||
![]() |
d38b8fc45c | ||
![]() |
5f92810f76 | ||
![]() |
34e12fcb38 | ||
![]() |
f769afd3ac | ||
![]() |
e1d78c3de6 | ||
![]() |
15a4023208 | ||
![]() |
cbb7415a18 | ||
![]() |
1a11e085b2 | ||
![]() |
fcb65940d9 | ||
![]() |
aa10e084a4 | ||
![]() |
b2be4aba53 | ||
![]() |
a1e66a4dc1 | ||
![]() |
6240243251 | ||
![]() |
ad58fca1f9 | ||
![]() |
f221ae53eb | ||
![]() |
5590c31ace | ||
![]() |
4e5754b285 | ||
![]() |
90a22af5e5 | ||
![]() |
77f8449c0c | ||
![]() |
be7ad16a3f | ||
![]() |
e28954f63e | ||
![]() |
52e60ac5b0 | ||
![]() |
6ffe5cd2a4 | ||
![]() |
9038357d78 | ||
![]() |
d56f6cb4f1 | ||
![]() |
5d301a4b33 | ||
![]() |
e65a7ff2ea | ||
![]() |
1bdc99ded7 | ||
![]() |
dd34492e16 | ||
![]() |
9f3b2f3167 | ||
![]() |
0260e34189 | ||
![]() |
5464cf674a | ||
![]() |
a58ce30e7b | ||
![]() |
83cecc0c09 | ||
![]() |
00757af149 | ||
![]() |
639eefcf1d | ||
![]() |
69a1f5f210 | ||
![]() |
0a74812eeb | ||
![]() |
cf437b6a33 | ||
![]() |
0ab7ebd017 | ||
![]() |
34e92b9f12 | ||
![]() |
86b1b75fb8 | ||
![]() |
4b9dcf377c | ||
![]() |
560318a5a7 | ||
![]() |
244ae878c9 | ||
![]() |
f9b7892976 | ||
![]() |
5f5b6fad0b | ||
![]() |
5c545c5e0b | ||
![]() |
20088a5c70 | ||
![]() |
099a4eb8c6 | ||
![]() |
ad7e519fb2 | ||
![]() |
df57c7f924 | ||
![]() |
7315255831 | ||
![]() |
eab7850ed6 | ||
![]() |
542e028977 | ||
![]() |
f131194b75 | ||
![]() |
d7e6afb01e | ||
![]() |
e1dcf378c3 | ||
![]() |
697c22a46c | ||
![]() |
7ca704be72 | ||
![]() |
72d381a3b6 | ||
![]() |
59c2520e0d | ||
![]() |
58d385241f | ||
![]() |
58059300bd | ||
![]() |
e4f2a450d6 | ||
![]() |
64bba77807 | ||
![]() |
a13b4270b5 | ||
![]() |
52c8fde461 | ||
![]() |
0a01aa28b0 | ||
![]() |
bfb202086d | ||
![]() |
6032c25813 | ||
![]() |
6cbb2fa5e1 | ||
![]() |
cdf301601f | ||
![]() |
1b974d1061 | ||
![]() |
602a913fa3 | ||
![]() |
6a8f24e973 | ||
![]() |
fde46885e9 | ||
![]() |
7223a51ba5 | ||
![]() |
8ac65d77e0 | ||
![]() |
65ebcf5384 | ||
![]() |
53caeb4565 | ||
![]() |
3b1cb0f58e | ||
![]() |
41ac46c7fe | ||
![]() |
8e3d737adc | ||
![]() |
7ef9e3dbe0 | ||
![]() |
78fcf1781a | ||
![]() |
2b08ed06af | ||
![]() |
0cdab04a64 | ||
![]() |
84aca3c009 | ||
![]() |
9662ccf486 | ||
![]() |
83719e8404 | ||
![]() |
04d90903a6 | ||
![]() |
f599b883cf | ||
![]() |
bef71e60b3 | ||
![]() |
acf4fc4193 | ||
![]() |
123dd8f011 | ||
![]() |
0516e3df45 | ||
![]() |
0c750084dc | ||
![]() |
907109b8bc | ||
![]() |
630aa730d5 | ||
![]() |
16faa26124 | ||
![]() |
ebabd20c98 | ||
![]() |
d40dfcd53c | ||
![]() |
6ab951caee | ||
![]() |
52e0993fa3 | ||
![]() |
d7bb5dfa8b | ||
![]() |
7c3d44c42e | ||
![]() |
dd6e7ec490 | ||
![]() |
2c1a863ffb | ||
![]() |
40382002f6 | ||
![]() |
05b4cb5546 | ||
![]() |
75dca80ac4 | ||
![]() |
cc56764ee9 | ||
![]() |
53f485d87e | ||
![]() |
f95cfb42c3 | ||
![]() |
26f5be1760 | ||
![]() |
d3f47097c1 | ||
![]() |
7a2092d3c4 | ||
![]() |
3e632600c6 | ||
![]() |
4df58b51ff | ||
![]() |
4fc8032c88 | ||
![]() |
5e1874eb8d | ||
![]() |
810391316c | ||
![]() |
47daef1e47 | ||
![]() |
294ad9fae1 | ||
![]() |
f1fe593fd6 | ||
![]() |
c982e8de67 | ||
![]() |
33339b1dc6 | ||
![]() |
ecf5af1e16 | ||
![]() |
4e77c46694 | ||
![]() |
3f8526160d | ||
![]() |
82fb3408ee | ||
![]() |
223c9319c7 | ||
![]() |
c61c2d8c3a | ||
![]() |
8bdf1e9044 | ||
![]() |
98dcc3f26e | ||
![]() |
e2b0ceae1d | ||
![]() |
5dba838533 | ||
![]() |
54eb28a097 | ||
![]() |
4bd4c78969 | ||
![]() |
1990bdcb52 | ||
![]() |
70bf274974 | ||
![]() |
a4fb8e769b | ||
![]() |
56bbc90c5b | ||
![]() |
2c54c696a1 | ||
![]() |
ddde10ec99 | ||
![]() |
fed5221503 | ||
![]() |
c0650f88d1 | ||
![]() |
5d5edd2639 | ||
![]() |
f77440efcb | ||
![]() |
74aa924625 | ||
![]() |
0338fc6f2d | ||
![]() |
12118c5454 | ||
![]() |
1bdb072ac8 | ||
![]() |
ade26794ec | ||
![]() |
8c4a08bb87 | ||
![]() |
34650f4b3c | ||
![]() |
59fa974b3b | ||
![]() |
98e8418087 | ||
![]() |
176064b520 | ||
![]() |
c5ad5589df | ||
![]() |
9987d335a0 | ||
![]() |
d6b44afb99 | ||
![]() |
7597ba9343 | ||
![]() |
41f1ad9f5f | ||
![]() |
29d3e72f49 | ||
![]() |
d04af1e392 | ||
![]() |
c620ddcba0 | ||
![]() |
e8aee7327b | ||
![]() |
018330c92c | ||
![]() |
58c048f1b0 | ||
![]() |
9f75d4597e | ||
![]() |
ffc48d3810 | ||
![]() |
c38b00dd07 | ||
![]() |
479c96745c | ||
![]() |
63807899a0 | ||
![]() |
a3806b6d7a | ||
![]() |
7bd87d1a82 | ||
![]() |
8bf18d6694 | ||
![]() |
06ee112344 | ||
![]() |
3cc43f63a0 | ||
![]() |
27bfa5a649 | ||
![]() |
5057e2caab | ||
![]() |
686fb31844 | ||
![]() |
03d5654a16 | ||
![]() |
83f0d72601 | ||
![]() |
19093e03ae | ||
![]() |
d28de71995 | ||
![]() |
f107485871 | ||
![]() |
984691f74c | ||
![]() |
8ba8e24fab | ||
![]() |
7c9433fd8b | ||
![]() |
821f395d8b |
4
AUTHORS
@@ -14,7 +14,7 @@ libtorrent (http://www.libtorrent.org):
|
||||
Contributors (and Past Developers):
|
||||
* Zach Tibbitts <zach@collegegeek.org>
|
||||
* Alon Zakai ('Kripken') <kripkensteiner@gmail.com>
|
||||
* Marcos Pinto ('markybob') <markybob@gmail.com>
|
||||
* Marcos Mobley ('markybob') <markybob@gmail.com>
|
||||
* Alex Dedul
|
||||
* Sadrul Habib Chowdhury
|
||||
* Ido Abramovich <ido.deluge@gmail.com>
|
||||
@@ -457,7 +457,7 @@ Translation Contributors:
|
||||
Marco Rodrigues
|
||||
Marcos
|
||||
Marcos Escalier
|
||||
Marcos Pinto
|
||||
Marcos Mobley
|
||||
Marcus Ekstrom
|
||||
Marek Dębowski
|
||||
Mário Buči
|
||||
|
257
ChangeLog
@@ -1,3 +1,255 @@
|
||||
=== Deluge 1.3.16 (unreleased) ===
|
||||
|
||||
==== Core ====
|
||||
* Fix saving copy of torrent file for magnet links.
|
||||
|
||||
=== Deluge 1.3.15 (12 May 2017) ===
|
||||
|
||||
==== Core ====
|
||||
* #2991: Fix issues with displaying libtorrent single proxy.
|
||||
* #3008: Fix libtorrent 1.2 trackers crashing Deluge UIs.
|
||||
* #2990: Fix error in torrent priorities causing file priority mismatch in UIs.
|
||||
|
||||
==== GtkUI ====
|
||||
* #3012: Configure gtkrc to use consistent button ordering on Windows.
|
||||
* Fix column sort state not saved in Thinclient mode.
|
||||
* #2786: Fix connection manager error with malformed ip.
|
||||
* #2866: Rename SystemTray/Indicator 'Pause/Resume All' to 'Pause/Resume Session'.
|
||||
* #2991: Workaround lt single proxy by greying out unused proxy types.
|
||||
|
||||
==== WebUI ====
|
||||
* Security Fix: Check render template files exist otherwise raise 404.
|
||||
|
||||
==== Notification Plugin ====
|
||||
* #2913: Fix webui passing string for int port value.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Add WebUI preferences page detailing lack of configuration via WebUI.
|
||||
|
||||
==== Label Plugin ====
|
||||
* Add WebUI preferences page detailing how to configure plugin.
|
||||
|
||||
|
||||
=== Deluge 1.3.14 (6 March 2017) ===
|
||||
|
||||
==== Core ====
|
||||
* #2889: Fixed 'Too many files open' errors.
|
||||
* #2861: Added support for python-geoip for use with libtorrent 1.1.
|
||||
* #2149: Fixed a single proxy entry being overwritten resulting in no proxy set.
|
||||
|
||||
==== UI ====
|
||||
* Added tracker_status translation to UIs.
|
||||
|
||||
==== GtkUI ====
|
||||
* #2901: Strip whitespace from infohash before checks.
|
||||
* Add missed feature autofill infohash entry from clipboard.
|
||||
|
||||
==== WebUI ====
|
||||
* #1908: Backport bind interface option for server.
|
||||
* Security: Fixed WebUI CSRF Vulnerability.
|
||||
|
||||
==== ConsoleUI ====
|
||||
* [#2948] [Console] Fix decode error comparing non-ascii (str) torrent name.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Fixes for splitting magnets from file.
|
||||
* Remove duplicate magnet extension when splitting.
|
||||
|
||||
|
||||
=== Deluge 1.3.13 (20 July 2016) ===
|
||||
==== Core ====
|
||||
* Increase RSA key size from 1024 to 2048 and use SHA256 digest.
|
||||
* Fixed empty error message from certain trackers.
|
||||
* Fixed torrent ending up displaying the wrong state.
|
||||
* #1032: Force a torrent into Error state if the resume data is rejected.
|
||||
* Workaround unwanted tracker announce when force rechecking paused torrent.
|
||||
* #2703: Stop moving torrent files if target files exist to prevent unintended clobbering of data.
|
||||
* #1330: Fixed the pausing and resuming of the Deluge session so torrents return to previous state.
|
||||
* #2765: Add support for TLS SNI in httpdownloader.
|
||||
* #2790: Ensure base32 magnet hash is uppercase to fix lowercase magnets uris.
|
||||
|
||||
==== Daemon ====
|
||||
* New command-line option to restict selected config key to read-only.
|
||||
* Allow use of uppercase log level to match UIs.
|
||||
|
||||
==== UI ====
|
||||
* #2832: Fixed error with blank lines in auth file.
|
||||
|
||||
==== GtkUI ====
|
||||
* Fixed installing plugin from a non-ascii directory.
|
||||
* Error'd torrents no longer display a progress percentage.
|
||||
* #2753: Fixed the 'Added' column showing the wrong date.
|
||||
* #2435: Prevent the user from changing tracker selection when editing trackers.
|
||||
* Fixed showing the wrong connected status with hostname in the Connection Manager.
|
||||
* #2754: Fixed the progress column to sort by progress and state correctly.
|
||||
* #2696: Fixed incorrect Move Completed folder shown in Options tab.
|
||||
* #2783: Sorting for name column is now case insensitive.
|
||||
* #2795: Reduce height of Add Torrent Dialog to help with smaller screeen resoltuions.
|
||||
* OSX: Fixed empty scrolling status (systray) menu.
|
||||
* OSX: Fixed starting deluged from connection manager.
|
||||
* #2093: Windows OS: Fixed opening non-ascii torrent files.
|
||||
* #2855: Fixed adding UDP trackers to trackers dialog.
|
||||
|
||||
==== WebUI ====
|
||||
* #2782: Fixed HTTPS negotiating incorrect cipher.
|
||||
* #2485: Fixed the broken Options context menu.
|
||||
* #2705: Fixed the hostlist config file not being created.
|
||||
* #2293: Fixed plugin's js code not loading when using the WebUI plugin.
|
||||
|
||||
==== Console ====
|
||||
* Fixed adding non-ascii torrent in non-interactive mode.
|
||||
* #2796: Add time_added to info sort keys.
|
||||
* #2815: Fixed 'add' cmd path inconsistency on Windows.
|
||||
|
||||
==== OSX Packaging ====
|
||||
* Source .py files no longer included in Deluge.app.
|
||||
|
||||
==== Windows OS Packaging ====
|
||||
* #2777: Updated MSVC SP1 check to latest release CLID.
|
||||
|
||||
==== Blocklist Plugin ====
|
||||
* #2729: Fixed plugin lockup with empty url.
|
||||
|
||||
==== Scheduler Plugin ====
|
||||
* Fixed corrupt plugin prefences page on OSX.
|
||||
* Fixed error accidentally introduced in 1.3.12.
|
||||
|
||||
==== Notification Plugin ====
|
||||
* #2402: Fixed the popup to show the actual count of files finished.
|
||||
* #2857: Fixed issue with SMTP port entry not updating in GTKUI.
|
||||
|
||||
==== AutoAdd Plugin ====
|
||||
* Fixed watch dir not accepting uppercase file extension.
|
||||
|
||||
==== Extractor Plugin ====
|
||||
* Ignore the remaining rar part files to prevent spawning useless processes.
|
||||
* #2785: Fixed only an empty folder when extracting rar files.
|
||||
|
||||
==== Execute Plugin ====
|
||||
* #2784: Windows OS: Escape ampersand in torrent args.
|
||||
|
||||
=== Deluge 1.3.12 (13 September 2015) ===
|
||||
==== GtkUI ====
|
||||
* #2731: Fix potential AttributeError in is_on_active_workspace
|
||||
|
||||
==== Core ====
|
||||
* Include fix for Twisted 15.0 URI class rename
|
||||
* #2233: Fix AttributeError in set_trackers with lt 1.0
|
||||
* Enable lt extension bindings again for versions >=0.16.7 (this disables Tracker Exchange by default)
|
||||
* Backport atomic fastresume and state file saving fixes as another attempt to prevent data loss on unclean exits
|
||||
|
||||
==== WebUI ====
|
||||
* Fixed i18n issue in Connection Manager which left users unable to connect
|
||||
* #2295: Increase cookie lifespan for display settings
|
||||
|
||||
==== Console ====
|
||||
* #2333: Fixed 'set and then get' in config command
|
||||
|
||||
==== Scheduler Plugin ====
|
||||
* Show current speed limit in statusbar
|
||||
|
||||
==== Win32 Packaging ====
|
||||
* #2736: Added version info to the properties of Deluge exes
|
||||
* #2734: Added a 256x256 to deluge.ico
|
||||
* #2325: Fixed the uninstaller deleting non-deluge files
|
||||
* Include pillow module to enable resizing of tracker icons
|
||||
|
||||
=== Deluge 1.3.11 (30 November 2014) ===
|
||||
==== GtkUI ====
|
||||
* Fixed ImportError for users with Twisted < 10
|
||||
* #2698: Fixed column issue when disabling a plugin
|
||||
|
||||
==== Core ====
|
||||
* Fixed cache issue with libtorrent 0.16 on Windows
|
||||
* #2555: Disabled use of SSLv3 protocol for DelugeRPC
|
||||
|
||||
==== WebUI ====
|
||||
* Modify SSL Context to allow >= TLSv1 protocol
|
||||
* #2588: Fixed Size column to show total_wanted instead of total_size
|
||||
|
||||
=== Deluge 1.3.10 (15 October 2014) ===
|
||||
==== GtkUI ====
|
||||
* #2256: Indexes are not updated correctly when removing column
|
||||
* Fix bug in the torrentview when Plugins added a column
|
||||
|
||||
==== WebUI ====
|
||||
* Security update for POODLE vulnerability
|
||||
|
||||
=== Deluge 1.3.9 (4 October 2014) ===
|
||||
==== GtkUI ====
|
||||
* #2514: Fix every torrent is displayed twice in classic mode
|
||||
|
||||
=== Deluge 1.3.8 (4 October 2014) ===
|
||||
==== Core ====
|
||||
* #1126 & #2322: Emit FinishedEvent after moving storage complete
|
||||
* Fixes to mitigate fastresume corruption
|
||||
|
||||
==== GtkUI ====
|
||||
* #2335: Fix application startup failing with 'cannot acquire lock' error
|
||||
* #2497: Fix the Queued Torrents 'Clear' button not properly clearing the list of torrent
|
||||
* #2496: Fix updating core_config before setting default options
|
||||
* #2493: Fix TypeError if active workspace is None
|
||||
* LP:#1168858: Nautilus window opens behind current window
|
||||
* Fix showing the open_folder menuitem
|
||||
* Suppress unimportant gnome warnings
|
||||
* Optimized the updating of the torrent view
|
||||
* Fixed Indicator icon label issue
|
||||
* Fix listview error with new config
|
||||
|
||||
==== WebUI ====
|
||||
* Ensure values are updated from config upon showing a plugin page
|
||||
|
||||
==== Extractor ====
|
||||
* Add WebUI plugin page
|
||||
* Find 7-zip application path on Windows via registry
|
||||
|
||||
==== Execute ====
|
||||
* #1290: Add a TorrentRemoved event option
|
||||
|
||||
==== Scheduler ====
|
||||
* #2238: Fix an 'undefined this.scheduleCells' error in javascript console
|
||||
|
||||
==== Notifications ====
|
||||
* #1310: Add WebUI plugin page
|
||||
|
||||
==== Blocklist ====
|
||||
* #2478: Add WebUI plugin page
|
||||
|
||||
==== Console ====
|
||||
* #2470: Fix console parsing args
|
||||
|
||||
=== Deluge 1.3.7 (9 July 2014) ===
|
||||
==== Core ====
|
||||
* #2324: Encryption level set by Deluge did not match libtorrent values
|
||||
* #2303: Torrent state was not updated until after emitting TorrentFinishedEvent
|
||||
* Fix twisted 13.1 compatability
|
||||
* #2343: Fix error if listen interface is whitespace
|
||||
* #2082: Validate ip address for listen_interface entry
|
||||
* #1490: Increase the Alertmanager interval to 0.3s
|
||||
* Prevent private flagged torrents auto-merging trackers
|
||||
|
||||
==== GtkUI ====
|
||||
* Fix issue with Plugins that add Tab to torrentdetails
|
||||
* Fix the scalable icon install directory
|
||||
* #2335: Fix IPC lockfile issue preventing start of deluge-gtk
|
||||
* #2365: Fix hiding Progress column generating TypeError
|
||||
* #2371: Add StartupWMClass to desktop file
|
||||
* #2372: Fix the Ratio column not retaining position
|
||||
* #2369: Fix bypassing the password dialog when showing/quitting
|
||||
|
||||
==== WebUI ====
|
||||
* #2374: Fix right-click selection issue
|
||||
* #2310: Fix unicode password support
|
||||
* #2418: Fix WebUI error when adding non-ascii torrent
|
||||
|
||||
==== Windows OS ====
|
||||
* Allow silent uninstall for Windows package
|
||||
* #2367: Fix DelugeStart theme not showing Private Flag as ticked/checked
|
||||
* #2315: Potential fix for lost window issue
|
||||
|
||||
==== Extractor ====
|
||||
* #2290: Fix dotted filenames being rejected
|
||||
|
||||
=== Deluge 1.3.6 (25 Feburary 2013) ===
|
||||
==== Core ====
|
||||
* Catch & log KeyError when removing a torrent from the queued torrents set
|
||||
@@ -75,6 +327,11 @@
|
||||
==== Execute ====
|
||||
* Fix execute plugin not working with unicode torrent names
|
||||
|
||||
==== Extractor ====
|
||||
* Add Windows support, using 7-zip
|
||||
* Added support for more extensions
|
||||
* Disabled extracting 'Move Completed' torrents due to race condition
|
||||
|
||||
=== Deluge 1.3.5 (09 April 2012) ===
|
||||
==== Core ====
|
||||
* Fix not properly detecting when torrent is at end of queue
|
||||
|
11
DEPENDS
@@ -11,20 +11,23 @@
|
||||
* chardet
|
||||
* geoip-database (optional)
|
||||
* setproctitle (optional)
|
||||
* pillow (optional)
|
||||
* python-geoip (optional)
|
||||
|
||||
* libtorrent >= 0.14, or build the included version
|
||||
* libtorrent (rasterbar) >= 0.14
|
||||
|
||||
* If building included libtorrent::
|
||||
* If building libtorrent:
|
||||
* boost >= 1.34.1
|
||||
* openssl
|
||||
* zlib
|
||||
|
||||
=== Gtk ===
|
||||
* python-notify (libnotify python wrapper)
|
||||
* pygame
|
||||
* pygtk >= 2.12
|
||||
* librsvg
|
||||
* xdg-utils
|
||||
* python-notify (optional)
|
||||
* pygame (optional)
|
||||
* python-appindicator (optional)
|
||||
|
||||
=== Web ===
|
||||
* mako
|
||||
|
102
deluge/common.py
@@ -36,6 +36,8 @@
|
||||
|
||||
"""Common functions for various parts of Deluge to use."""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
@@ -173,22 +175,21 @@ def get_default_download_dir():
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
return os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
else:
|
||||
download_dir = ""
|
||||
if not windows_check():
|
||||
from xdg.BaseDirectory import xdg_config_home
|
||||
userdir_file = os.path.join(xdg_config_home, 'user-dirs.dirs')
|
||||
try:
|
||||
for line in open(userdir_file, 'r'):
|
||||
if not line.startswith('#') and 'XDG_DOWNLOAD_DIR' in line:
|
||||
download_dir = os.path.expandvars(\
|
||||
line.partition("=")[2].rstrip().strip('"'))
|
||||
if os.path.isdir(download_dir):
|
||||
return download_dir
|
||||
with open(os.path.join(xdg_config_home, 'user-dirs.dirs'), 'r') 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:
|
||||
pass
|
||||
|
||||
return os.environ.get("HOME")
|
||||
if not download_dir:
|
||||
download_dir = os.path.join(os.path.expanduser("~"), 'Downloads')
|
||||
return download_dir
|
||||
|
||||
def windows_check():
|
||||
"""
|
||||
@@ -233,12 +234,14 @@ def get_pixmap(fname):
|
||||
return pkg_resources.resource_filename("deluge", os.path.join("data", \
|
||||
"pixmaps", fname))
|
||||
|
||||
def open_file(path):
|
||||
def open_file(path, timestamp=None):
|
||||
"""
|
||||
Opens a file or folder using the system configured program
|
||||
|
||||
:param path: the path to the file or folder to open
|
||||
:type path: string
|
||||
:param timestamp: the timestamp of the event that requested to open
|
||||
:type timestamp: int
|
||||
|
||||
"""
|
||||
if windows_check():
|
||||
@@ -246,7 +249,12 @@ def open_file(path):
|
||||
elif osx_check():
|
||||
subprocess.Popen(["open", "%s" % path])
|
||||
else:
|
||||
subprocess.Popen(["xdg-open", "%s" % path])
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
env = os.environ.copy()
|
||||
env["DESKTOP_STARTUP_ID"] = "%s-%u-%s-xdg_open_TIME%d" % \
|
||||
(os.path.basename(sys.argv[0]), os.getpid(), os.uname()[1], timestamp)
|
||||
subprocess.Popen(["xdg-open", "%s" % path], env=env)
|
||||
|
||||
def open_url_in_browser(url):
|
||||
"""
|
||||
@@ -525,7 +533,7 @@ def free_space(path):
|
||||
:raises InvalidPathError: if the path is not valid
|
||||
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
if not path or not os.path.exists(path):
|
||||
raise InvalidPathError("%s is not a valid path" % path)
|
||||
|
||||
if windows_check():
|
||||
@@ -628,37 +636,55 @@ def xml_encode(string):
|
||||
|
||||
def decode_string(s, encoding="utf8"):
|
||||
"""
|
||||
Decodes a string and re-encodes it in utf8. If it cannot decode using
|
||||
`:param:encoding` then it will try to detect the string encoding and
|
||||
decode it.
|
||||
Decodes a string and return unicode. If it cannot decode using
|
||||
`:param:encoding` then it will try latin1, and if that fails,
|
||||
try to detect the string encoding. If that fails, decode with
|
||||
ignore.
|
||||
|
||||
:param s: string to decode
|
||||
:type s: string
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: s converted to unicode
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
if not s:
|
||||
return u''
|
||||
elif isinstance(s, unicode):
|
||||
return s
|
||||
|
||||
try:
|
||||
s = s.decode(encoding).encode("utf8", "ignore")
|
||||
except UnicodeDecodeError:
|
||||
s = s.decode(chardet.detect(s)["encoding"], "ignore").encode("utf8", "ignore")
|
||||
return s
|
||||
encodings = [lambda: ("utf8", 'strict'),
|
||||
lambda: ("iso-8859-1", 'strict'),
|
||||
lambda: (chardet.detect(s)["encoding"], 'strict'),
|
||||
lambda: (encoding, 'ignore')]
|
||||
|
||||
def utf8_encoded(s):
|
||||
if not encoding is "utf8":
|
||||
encodings.insert(0, lambda: (encoding, 'strict'))
|
||||
|
||||
for l in encodings:
|
||||
try:
|
||||
return s.decode(*l())
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return u''
|
||||
|
||||
def utf8_encoded(s, encoding="utf8"):
|
||||
"""
|
||||
Returns a utf8 encoded string of s
|
||||
|
||||
:param s: (unicode) string to (re-)encode
|
||||
:type s: basestring
|
||||
:keyword encoding: the encoding to use in the decoding
|
||||
:type encoding: string
|
||||
:returns: a utf8 encoded string of s
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
s = decode_string(s)
|
||||
s = decode_string(s, encoding).encode("utf8")
|
||||
elif isinstance(s, unicode):
|
||||
s = s.encode("utf8", "ignore")
|
||||
s = s.encode("utf8")
|
||||
return s
|
||||
|
||||
class VersionSplit(object):
|
||||
@@ -672,7 +698,7 @@ class VersionSplit(object):
|
||||
def __init__(self, ver):
|
||||
ver = ver.lower()
|
||||
vs = ver.replace("_", "-").split("-")
|
||||
self.version = [int(x) for x in vs[0].split(".")]
|
||||
self.version = [int(x) for x in vs[0].split(".") if x.isdigit()]
|
||||
self.suffix = None
|
||||
self.dev = False
|
||||
if len(vs) > 1:
|
||||
@@ -695,3 +721,27 @@ class VersionSplit(object):
|
||||
v1 = [self.version, self.suffix or 'z', self.dev]
|
||||
v2 = [ver.version, ver.suffix or 'z', ver.dev]
|
||||
return cmp(v1, v2)
|
||||
|
||||
def win32_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 xrange(start, argc.value)]
|
||||
|
@@ -67,6 +67,8 @@ version as this will be done internally.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import cPickle as pickle
|
||||
import shutil
|
||||
import os
|
||||
@@ -109,8 +111,13 @@ def find_json_objects(s):
|
||||
if start < 0:
|
||||
return []
|
||||
|
||||
quoted = False
|
||||
for index, c in enumerate(s[offset:]):
|
||||
if c == "{":
|
||||
if c == '"':
|
||||
quoted = not quoted
|
||||
elif quoted:
|
||||
continue
|
||||
elif c == "{":
|
||||
opens += 1
|
||||
elif c == "}":
|
||||
opens -= 1
|
||||
@@ -356,7 +363,8 @@ what is currently in the config and it could not convert the value
|
||||
filename = self.__config_file
|
||||
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
with open(filename, "rb") as _file:
|
||||
data = _file.read()
|
||||
except IOError, e:
|
||||
log.warning("Unable to open config file %s: %s", filename, e)
|
||||
return
|
||||
@@ -404,7 +412,8 @@ what is currently in the config and it could not convert the value
|
||||
# Check to see if the current config differs from the one on disk
|
||||
# We will only write a new config file if there is a difference
|
||||
try:
|
||||
data = open(filename, "rb").read()
|
||||
with open(filename, "rb") as _file:
|
||||
data = _file.read()
|
||||
objects = find_json_objects(data)
|
||||
start, end = objects[0]
|
||||
version = json.loads(data[start:end])
|
||||
|
@@ -51,7 +51,7 @@ from deluge.log import LOG as log
|
||||
class AlertManager(component.Component):
|
||||
def __init__(self):
|
||||
log.debug("AlertManager initialized..")
|
||||
component.Component.__init__(self, "AlertManager", interval=0.05)
|
||||
component.Component.__init__(self, "AlertManager", interval=0.3)
|
||||
self.session = component.get("Core").session
|
||||
|
||||
self.session.set_alert_mask(
|
||||
@@ -74,7 +74,8 @@ class AlertManager(component.Component):
|
||||
|
||||
def stop(self):
|
||||
for dc in self.delayed_calls:
|
||||
dc.cancel()
|
||||
if dc.active():
|
||||
dc.cancel()
|
||||
self.delayed_calls = []
|
||||
|
||||
def register_handler(self, alert_type, handler):
|
||||
|
@@ -33,6 +33,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
@@ -118,7 +120,8 @@ class AuthManager(component.Component):
|
||||
f = [localclient]
|
||||
else:
|
||||
# Load the auth file into a dictionary: {username: password, ...}
|
||||
f = open(auth_file, "r").readlines()
|
||||
with open(auth_file, "r") as _file:
|
||||
f = _file.readlines()
|
||||
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -143,4 +146,5 @@ class AuthManager(component.Component):
|
||||
self.__auth[username.strip()] = (password.strip(), level)
|
||||
|
||||
if "localclient" not in self.__auth:
|
||||
open(auth_file, "a").write(self.__create_localclient_account())
|
||||
with open(auth_file, "a") as _file:
|
||||
_file.write(self.__create_localclient_account())
|
||||
|
@@ -33,6 +33,8 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
from deluge._libtorrent import lt
|
||||
|
||||
import os
|
||||
@@ -72,22 +74,30 @@ from deluge.core.eventmanager import EventManager
|
||||
from deluge.core.rpcserver import export
|
||||
|
||||
class Core(component.Component):
|
||||
def __init__(self, listen_interface=None):
|
||||
def __init__(self, listen_interface=None, read_only_config_keys=None):
|
||||
log.debug("Core init..")
|
||||
component.Component.__init__(self, "Core")
|
||||
|
||||
# These keys will be dropped from the set_config() RPC and are
|
||||
# configurable from the command-line.
|
||||
self.read_only_config_keys = read_only_config_keys
|
||||
log.debug("read_only_config_keys: %s", read_only_config_keys)
|
||||
|
||||
# Start the libtorrent session
|
||||
log.info("Starting libtorrent %s session..", lt.version)
|
||||
|
||||
# Create the client fingerprint
|
||||
version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")]
|
||||
version = deluge.common.VersionSplit(deluge.common.get_version()).version
|
||||
while len(version) < 4:
|
||||
version.append(0)
|
||||
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
# Setting session flags to 1 enables all libtorrent default plugins
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=1)
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=0)
|
||||
else:
|
||||
# Setting session flags to 1 enables all libtorrent default plugins
|
||||
self.session = lt.session(lt.fingerprint("DE", *version), flags=1)
|
||||
|
||||
# Load the session state if available
|
||||
self.__load_session_state()
|
||||
@@ -97,10 +107,12 @@ class Core(component.Component):
|
||||
self.settings.user_agent = "Deluge %s" % deluge.common.get_version()
|
||||
# Increase the alert queue size so that alerts don't get lost
|
||||
self.settings.alert_queue_size = 10000
|
||||
# Ignore buggy resume data timestamps checking #3044.
|
||||
self.settings.ignore_resume_timestamps = True
|
||||
|
||||
# Set session settings
|
||||
self.settings.send_redundant_have = True
|
||||
if deluge.common.windows_check():
|
||||
if deluge.common.windows_check() and lt.version_major == 0 and lt.version_minor <= 15:
|
||||
self.settings.disk_io_write_mode = \
|
||||
lt.io_buffer_mode_t.disable_os_cache
|
||||
self.settings.disk_io_read_mode = \
|
||||
@@ -108,11 +120,12 @@ class Core(component.Component):
|
||||
self.session.set_settings(self.settings)
|
||||
|
||||
# Load metadata extension
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
# self.session.add_extension(lt.create_metadata_plugin)
|
||||
# self.session.add_extension(lt.create_ut_metadata_plugin)
|
||||
# self.session.add_extension(lt.create_smart_ban_plugin)
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session.add_extension("metadata_transfer")
|
||||
self.session.add_extension("ut_metadata")
|
||||
self.session.add_extension("smart_ban")
|
||||
|
||||
# Create the components
|
||||
self.eventmanager = EventManager()
|
||||
@@ -127,6 +140,9 @@ class Core(component.Component):
|
||||
# New release check information
|
||||
self.new_release = None
|
||||
|
||||
# GeoIP instance with db loaded
|
||||
self.geoip_instance = None
|
||||
|
||||
# Get the core config
|
||||
self.config = deluge.configmanager.ConfigManager("core.conf")
|
||||
|
||||
@@ -134,8 +150,11 @@ class Core(component.Component):
|
||||
# store the one in the config so we can restore it on shutdown
|
||||
self.__old_interface = None
|
||||
if listen_interface:
|
||||
self.__old_interface = self.config["listen_interface"]
|
||||
self.config["listen_interface"] = listen_interface
|
||||
if deluge.common.is_ip(listen_interface):
|
||||
self.__old_interface = self.config["listen_interface"]
|
||||
self.config["listen_interface"] = listen_interface
|
||||
else:
|
||||
log.error("Invalid listen interface (must be IP Address): %s", listen_interface)
|
||||
|
||||
def start(self):
|
||||
"""Starts the core"""
|
||||
@@ -174,8 +193,8 @@ class Core(component.Component):
|
||||
def __load_session_state(self):
|
||||
"""Loads the libtorrent session state"""
|
||||
try:
|
||||
self.session.load_state(lt.bdecode(
|
||||
open(deluge.configmanager.get_config_dir("session.state"), "rb").read()))
|
||||
with open(deluge.configmanager.get_config_dir("session.state"), "rb") as _file:
|
||||
self.session.load_state(lt.bdecode(_file.read()))
|
||||
except Exception, e:
|
||||
log.warning("Failed to load lt state: %s", e)
|
||||
|
||||
@@ -277,7 +296,7 @@ class Core(component.Component):
|
||||
result.addCallbacks(on_download_success, on_download_fail)
|
||||
else:
|
||||
# Log the error and pass the failure onto the client
|
||||
log.error("Error occured downloading torrent from %s", url)
|
||||
log.error("Error occurred downloading torrent from %s", url)
|
||||
log.error("Reason: %s", failure.getErrorMessage())
|
||||
result = failure
|
||||
return result
|
||||
@@ -402,15 +421,18 @@ class Core(component.Component):
|
||||
@export
|
||||
def pause_all_torrents(self):
|
||||
"""Pause all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.pause()
|
||||
if not self.session.is_paused():
|
||||
self.session.pause()
|
||||
component.get("EventManager").emit(SessionPausedEvent())
|
||||
|
||||
@export
|
||||
def resume_all_torrents(self):
|
||||
"""Resume all torrents in the session"""
|
||||
for torrent in self.torrentmanager.torrents.values():
|
||||
torrent.resume()
|
||||
component.get("EventManager").emit(SessionResumedEvent())
|
||||
if self.session.is_paused():
|
||||
self.session.resume()
|
||||
for torrent_id in self.torrentmanager.torrents:
|
||||
self.torrentmanager[torrent_id].update_state()
|
||||
component.get("EventManager").emit(SessionResumedEvent())
|
||||
|
||||
@export
|
||||
def resume_torrent(self, torrent_ids):
|
||||
@@ -427,9 +449,9 @@ class Core(component.Component):
|
||||
# Torrent was probaly removed meanwhile
|
||||
return {}
|
||||
|
||||
# Get the leftover fields and ask the plugin manager to fill them
|
||||
# Get any remaining keys from plugin manager or 'all' if no keys specified.
|
||||
leftover_fields = list(set(keys) - set(status.keys()))
|
||||
if len(leftover_fields) > 0:
|
||||
if len(leftover_fields) > 0 or len(keys) == 0:
|
||||
status.update(self.pluginmanager.get_status(torrent_id, leftover_fields))
|
||||
return status
|
||||
|
||||
@@ -492,6 +514,8 @@ class Core(component.Component):
|
||||
"""Set the config with values from dictionary"""
|
||||
# Load all the values into the configuration
|
||||
for key in config.keys():
|
||||
if self.read_only_config_keys and key in self.read_only_config_keys:
|
||||
continue
|
||||
if isinstance(config[key], unicode) or isinstance(config[key], str):
|
||||
config[key] = config[key].encode("utf8")
|
||||
self.config[key] = config[key]
|
||||
@@ -644,7 +668,9 @@ class Core(component.Component):
|
||||
if add_to_session:
|
||||
options = {}
|
||||
options["download_location"] = os.path.split(path)[0]
|
||||
self.add_torrent_file(os.path.split(target)[1], open(target, "rb").read(), options)
|
||||
with open(target, "rb") as _file:
|
||||
filedump = base64.encodestring(_file.read())
|
||||
self.add_torrent_file(os.path.split(target)[1], filedump, options)
|
||||
|
||||
@export
|
||||
def upload_plugin(self, filename, filedump):
|
||||
|
@@ -32,6 +32,8 @@
|
||||
# statement from all source files in the program, then also delete it here.
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import gettext
|
||||
import locale
|
||||
@@ -52,7 +54,8 @@ class Daemon(object):
|
||||
if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")):
|
||||
# Get the PID and the port of the supposedly running daemon
|
||||
try:
|
||||
(pid, port) = open(deluge.configmanager.get_config_dir("deluged.pid")).read().strip().split(";")
|
||||
with open(deluge.configmanager.get_config_dir("deluged.pid")) as _file:
|
||||
(pid, port) = _file.read().strip().split(";")
|
||||
pid = int(pid)
|
||||
port = int(port)
|
||||
except ValueError:
|
||||
@@ -133,9 +136,15 @@ class Daemon(object):
|
||||
else:
|
||||
listen_interface = ""
|
||||
|
||||
if options and options.read_only_config_keys:
|
||||
read_only_config_keys = options.read_only_config_keys.split(",")
|
||||
else:
|
||||
read_only_config_keys = []
|
||||
|
||||
from deluge.core.core import Core
|
||||
# Start the core as a thread and join it until it's done
|
||||
self.core = Core(listen_interface=listen_interface)
|
||||
self.core = Core(listen_interface=listen_interface,
|
||||
read_only_config_keys=read_only_config_keys)
|
||||
|
||||
port = self.core.config["daemon_port"]
|
||||
if options and options.port:
|
||||
@@ -163,8 +172,8 @@ class Daemon(object):
|
||||
if not classic:
|
||||
# Write out a pid file all the time, we use this to see if a deluged is running
|
||||
# We also include the running port number to do an additional test
|
||||
open(deluge.configmanager.get_config_dir("deluged.pid"), "wb").write(
|
||||
"%s;%s\n" % (os.getpid(), port))
|
||||
with open(deluge.configmanager.get_config_dir("deluged.pid"), "wb") as _file:
|
||||
_file.write("%s;%s\n" % (os.getpid(), port))
|
||||
|
||||
component.start()
|
||||
try:
|
||||
|
@@ -91,7 +91,7 @@ def tracker_error_filter(torrent_ids, values):
|
||||
# Check all the torrent's tracker_status for 'Error:' and only return torrent_ids
|
||||
# that have this substring in their tracker_status
|
||||
for torrent_id in torrent_ids:
|
||||
if _("Error") + ":" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
if "Error:" in tm[torrent_id].get_status(["tracker_status"])["tracker_status"]:
|
||||
filtered_torrent_ids.append(torrent_id)
|
||||
|
||||
return filtered_torrent_ids
|
||||
@@ -169,7 +169,9 @@ class FilterManager(component.Component):
|
||||
for torrent_id in list(torrent_ids):
|
||||
status = status_func(torrent_id, filter_dict.keys()) #status={key:value}
|
||||
for field, values in filter_dict.iteritems():
|
||||
if (not status[field] in values) and torrent_id in torrent_ids:
|
||||
if field in status and status[field] in values:
|
||||
continue
|
||||
elif torrent_id in torrent_ids:
|
||||
torrent_ids.remove(torrent_id)
|
||||
|
||||
return torrent_ids
|
||||
|
@@ -92,6 +92,8 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
|
||||
def get_status(self, torrent_id, fields):
|
||||
"""Return the value of status fields for the selected torrent_id."""
|
||||
status = {}
|
||||
if len(fields) == 0:
|
||||
fields = self.status_fields.keys()
|
||||
for field in fields:
|
||||
try:
|
||||
status[field] = self.status_fields[field](torrent_id)
|
||||
|
@@ -33,6 +33,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os.path
|
||||
import threading
|
||||
@@ -48,6 +49,12 @@ import deluge.common
|
||||
import deluge.component as component
|
||||
from deluge.log import LOG as log
|
||||
|
||||
try:
|
||||
import GeoIP
|
||||
except ImportError:
|
||||
GeoIP = None
|
||||
|
||||
|
||||
DEFAULT_PREFS = {
|
||||
"send_info": False,
|
||||
"info_sent": 0.0,
|
||||
@@ -144,6 +151,8 @@ DEFAULT_PREFS = {
|
||||
}
|
||||
|
||||
class PreferencesManager(component.Component):
|
||||
LT_SINGLE_PROXY = deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.0.0")
|
||||
|
||||
def __init__(self):
|
||||
component.Component.__init__(self, "PreferencesManager")
|
||||
|
||||
@@ -251,7 +260,7 @@ class PreferencesManager(component.Component):
|
||||
# Only set the listen ports if random_port is not true
|
||||
if self.config["random_port"] is not True:
|
||||
log.debug("listen port range set to %s-%s", value[0], value[1])
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(value[0], value[1], str(self.config["listen_interface"]).strip())
|
||||
|
||||
def _on_set_listen_interface(self, key, value):
|
||||
# Call the random_port callback since it'll do what we need
|
||||
@@ -273,7 +282,7 @@ class PreferencesManager(component.Component):
|
||||
# Set the listen ports
|
||||
log.debug("listen port range set to %s-%s", listen_ports[0],
|
||||
listen_ports[1])
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]))
|
||||
self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"]).strip())
|
||||
|
||||
def _on_set_outgoing_ports(self, key, value):
|
||||
if not self.config["random_outgoing_ports"]:
|
||||
@@ -298,7 +307,8 @@ class PreferencesManager(component.Component):
|
||||
if value:
|
||||
state = None
|
||||
try:
|
||||
state = lt.bdecode(open(state_file, "rb").read())
|
||||
with open(state_file, "rb") as _file:
|
||||
state = lt.bdecode(_file.read())
|
||||
except Exception, e:
|
||||
log.warning("Unable to read DHT state file: %s", e)
|
||||
|
||||
@@ -309,6 +319,8 @@ class PreferencesManager(component.Component):
|
||||
self.session.start_dht(None)
|
||||
self.session.add_dht_router("router.bittorrent.com", 6881)
|
||||
self.session.add_dht_router("router.utorrent.com", 6881)
|
||||
self.session.add_dht_router("dht.transmissionbt.com", 6881)
|
||||
self.session.add_dht_router("dht.aelitis.com", 6881)
|
||||
self.session.add_dht_router("router.bitcomet.com", 6881)
|
||||
else:
|
||||
self.core.save_dht_state()
|
||||
@@ -337,19 +349,19 @@ class PreferencesManager(component.Component):
|
||||
|
||||
def _on_set_utpex(self, key, value):
|
||||
log.debug("utpex value set to %s", value)
|
||||
if value:
|
||||
# Note: All libtorrent python bindings to set plugins/extensions need to be disabled
|
||||
# due to GIL issue. https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
#self.session.add_extension(lt.create_ut_pex_plugin)
|
||||
pass
|
||||
# In libtorrent versions below 0.16.7.0 disable extension bindings due to GIL issue.
|
||||
# https://code.google.com/p/libtorrent/issues/detail?id=369
|
||||
if value and deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("0.16.7.0"):
|
||||
self.session.add_extension("ut_pex")
|
||||
|
||||
def _on_set_encryption(self, key, value):
|
||||
log.debug("encryption value %s set to %s..", key, value)
|
||||
pe_enc_level = {0: lt.enc_level.plaintext, 1: lt.enc_level.rc4, 2: lt.enc_level.both}
|
||||
pe_settings = lt.pe_settings()
|
||||
pe_settings.out_enc_policy = \
|
||||
lt.enc_policy(self.config["enc_out_policy"])
|
||||
pe_settings.in_enc_policy = lt.enc_policy(self.config["enc_in_policy"])
|
||||
pe_settings.allowed_enc_level = lt.enc_level(self.config["enc_level"])
|
||||
pe_settings.allowed_enc_level = lt.enc_level(pe_enc_level[self.config["enc_level"]])
|
||||
pe_settings.prefer_rc4 = self.config["enc_prefer_rc4"]
|
||||
self.session.set_pe_settings(pe_settings)
|
||||
set = self.session.get_pe_settings()
|
||||
@@ -470,15 +482,34 @@ class PreferencesManager(component.Component):
|
||||
self.new_release_timer.stop()
|
||||
|
||||
def _on_set_proxies(self, key, value):
|
||||
for k, v in value.items():
|
||||
# Test for single proxy with lt >= 0.16
|
||||
if self.LT_SINGLE_PROXY:
|
||||
for proxy_type in value:
|
||||
if proxy_type == "peer":
|
||||
continue
|
||||
if self.config["proxies"][proxy_type] != value["peer"]:
|
||||
log.warning("This version of libtorrent only supports a single proxy setting "
|
||||
"based upon 'peer' which will apply to all other other types.")
|
||||
self.config["proxies"][proxy_type] = value["peer"]
|
||||
|
||||
proxy_settings = lt.proxy_settings()
|
||||
proxy_settings.type = lt.proxy_type(v["type"])
|
||||
proxy_settings.username = str(v["username"])
|
||||
proxy_settings.password = str(v["password"])
|
||||
proxy_settings.hostname = str(v["hostname"])
|
||||
proxy_settings.port = v["port"]
|
||||
log.debug("setting %s proxy settings", k)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
proxy_settings.type = lt.proxy_type(value["peer"]["type"])
|
||||
proxy_settings.username = str(value["peer"]["username"])
|
||||
proxy_settings.password = str(value["peer"]["password"])
|
||||
proxy_settings.hostname = str(value["peer"]["hostname"])
|
||||
proxy_settings.port = value["peer"]["port"]
|
||||
log.debug("Setting proxy settings: %s", value["peer"])
|
||||
self.session.set_proxy(proxy_settings)
|
||||
else:
|
||||
for k, v in value.items():
|
||||
proxy_settings = lt.proxy_settings()
|
||||
proxy_settings.type = lt.proxy_type(v["type"])
|
||||
proxy_settings.username = str(v["username"])
|
||||
proxy_settings.password = str(v["password"])
|
||||
proxy_settings.hostname = str(v["hostname"])
|
||||
proxy_settings.port = v["port"]
|
||||
log.debug("Setting %s proxy settings: %s", k, v)
|
||||
getattr(self.session, "set_%s_proxy" % k)(proxy_settings)
|
||||
|
||||
def _on_rate_limit_ip_overhead(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
@@ -487,21 +518,19 @@ class PreferencesManager(component.Component):
|
||||
def _on_geoip_db_location(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
# Load the GeoIP DB for country look-ups if available
|
||||
geoip_db = ""
|
||||
if os.path.exists(value):
|
||||
geoip_db = value
|
||||
elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))):
|
||||
geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))
|
||||
try:
|
||||
self.core.geoip_instance = GeoIP.open(value, GeoIP.GEOIP_STANDARD)
|
||||
except AttributeError:
|
||||
try:
|
||||
self.session.load_country_db(value)
|
||||
except RuntimeError, ex:
|
||||
log.error("Unable to load geoip database: %s", ex)
|
||||
except AttributeError:
|
||||
log.warning("GeoIP Unavailable")
|
||||
else:
|
||||
log.warning("Unable to find GeoIP database file!")
|
||||
|
||||
if geoip_db:
|
||||
try:
|
||||
self.session.load_country_db(str(geoip_db))
|
||||
except Exception, e:
|
||||
log.error("Unable to load geoip database!")
|
||||
log.exception(e)
|
||||
|
||||
def _on_cache_size(self, key, value):
|
||||
log.debug("%s: %s", key, value)
|
||||
self.session_set_setting("cache_size", value)
|
||||
|
@@ -35,6 +35,8 @@
|
||||
|
||||
"""RPCServer Module"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
import os
|
||||
@@ -131,7 +133,8 @@ class ServerContextFactory(object):
|
||||
SSL transport.
|
||||
"""
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
ctx = SSL.Context(SSL.SSLv3_METHOD)
|
||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
||||
ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||
ctx.use_certificate_file(os.path.join(ssl_dir, "daemon.cert"))
|
||||
ctx.use_privatekey_file(os.path.join(ssl_dir, "daemon.pkey"))
|
||||
return ctx
|
||||
@@ -201,8 +204,8 @@ class DelugeRPCProtocol(Protocol):
|
||||
"""
|
||||
peer = self.transport.getPeer()
|
||||
log.info("Deluge Client connection made from: %s:%s", peer.host, peer.port)
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE
|
||||
# Set the initial auth level of this session to AUTH_LEVEL_NONE and empty username.
|
||||
self.factory.authorized_sessions[self.transport.sessionno] = (AUTH_LEVEL_NONE, "")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
@@ -492,10 +495,10 @@ def generate_ssl_keys():
|
||||
"""
|
||||
This method generates a new SSL key/cert.
|
||||
"""
|
||||
digest = "md5"
|
||||
digest = "sha256"
|
||||
# Generate key pair
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 1024)
|
||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||
|
||||
# Generate cert request
|
||||
req = crypto.X509Req()
|
||||
@@ -508,7 +511,7 @@ def generate_ssl_keys():
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(0)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60*60*24*365*5) # Five Years
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
|
||||
cert.set_issuer(req.get_subject())
|
||||
cert.set_subject(req.get_subject())
|
||||
cert.set_pubkey(req.get_pubkey())
|
||||
@@ -516,8 +519,10 @@ def generate_ssl_keys():
|
||||
|
||||
# Write out files
|
||||
ssl_dir = deluge.configmanager.get_config_dir("ssl")
|
||||
open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
with open(os.path.join(ssl_dir, "daemon.pkey"), "w") as _file:
|
||||
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(os.path.join(ssl_dir, "daemon.cert"), "w") 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)
|
||||
|
@@ -34,6 +34,8 @@
|
||||
|
||||
"""Internal Torrent class"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import time
|
||||
from urllib import unquote
|
||||
@@ -98,6 +100,14 @@ class TorrentOptions(dict):
|
||||
self["file_priorities"] = []
|
||||
self["mapped_files"] = {}
|
||||
|
||||
|
||||
class TorrentError(object):
|
||||
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):
|
||||
"""Torrent holds information about torrents added to the libtorrent session.
|
||||
"""
|
||||
@@ -134,13 +144,16 @@ class Torrent(object):
|
||||
# We store the filename just in case we need to make a copy of the torrentfile
|
||||
if not filename:
|
||||
# If no filename was provided, then just use the infohash
|
||||
filename = self.torrent_id
|
||||
filename = self.torrent_id + '.torrent'
|
||||
|
||||
self.filename = filename
|
||||
|
||||
# Store the magnet uri used to add this torrent if available
|
||||
self.magnet = magnet
|
||||
|
||||
# Torrent state e.g. Paused, Downloading, etc.
|
||||
self.state = None
|
||||
|
||||
# Holds status info so that we don't need to keep getting it from lt
|
||||
self.status = self.handle.status()
|
||||
|
||||
@@ -170,26 +183,20 @@ class Torrent(object):
|
||||
self.filename = state.filename
|
||||
self.is_finished = state.is_finished
|
||||
else:
|
||||
# Tracker list
|
||||
self.trackers = []
|
||||
# Create a list of trackers
|
||||
for value in self.handle.trackers():
|
||||
if lt.version_major == 0 and lt.version_minor < 15:
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
else:
|
||||
tracker = value
|
||||
self.trackers.append(tracker)
|
||||
# Set trackers from libtorrent
|
||||
self.set_trackers(None)
|
||||
|
||||
# Various torrent options
|
||||
self.handle.resolve_countries(True)
|
||||
|
||||
self.set_options(self.options)
|
||||
# Details of torrent forced into error state (i.e. not by libtorrent).
|
||||
self.forced_error = None
|
||||
|
||||
# Status message holds error info about the torrent
|
||||
self.statusmsg = "OK"
|
||||
|
||||
self.set_options(self.options)
|
||||
|
||||
# The torrents state
|
||||
self.update_state()
|
||||
|
||||
@@ -209,8 +216,6 @@ class Torrent(object):
|
||||
self.forcing_recheck = False
|
||||
self.forcing_recheck_paused = False
|
||||
|
||||
log.debug("Torrent object created.")
|
||||
|
||||
## Options methods ##
|
||||
def set_options(self, options):
|
||||
OPTIONS_FUNCS = {
|
||||
@@ -235,6 +240,10 @@ class Torrent(object):
|
||||
|
||||
|
||||
def set_max_connections(self, max_connections):
|
||||
if max_connections == 0:
|
||||
max_connections = -1
|
||||
elif max_connections == 1:
|
||||
max_connections = 2
|
||||
self.options["max_connections"] = int(max_connections)
|
||||
self.handle.set_max_connections(max_connections)
|
||||
|
||||
@@ -292,19 +301,19 @@ class Torrent(object):
|
||||
self.options["move_completed_path"] = move_completed_path
|
||||
|
||||
def set_file_priorities(self, file_priorities):
|
||||
if len(file_priorities) != len(self.get_files()):
|
||||
log.debug("file_priorities len != num_files")
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
return
|
||||
|
||||
if self.options["compact_allocation"]:
|
||||
log.debug("setting file priority with compact allocation does not work!")
|
||||
self.options["file_priorities"] = self.handle.file_priorities()
|
||||
return
|
||||
handle_file_priorities = self.handle.file_priorities()
|
||||
# Workaround for libtorrent 1.1 changing default priorities from 1 to 4.
|
||||
if 4 in handle_file_priorities:
|
||||
handle_file_priorities = [1 if x == 4 else x for x in handle_file_priorities]
|
||||
|
||||
log.debug("setting %s's file priorities: %s", self.torrent_id, file_priorities)
|
||||
|
||||
self.handle.prioritize_files(file_priorities)
|
||||
if (self.handle.has_metadata() and not self.options["compact_allocation"] and
|
||||
file_priorities and len(file_priorities) == len(self.get_files())):
|
||||
self.handle.prioritize_files(file_priorities)
|
||||
else:
|
||||
log.debug("Unable to set new file priorities.")
|
||||
file_priorities = handle_file_priorities
|
||||
|
||||
if 0 in self.options["file_priorities"]:
|
||||
# We have previously marked a file 'Do Not Download'
|
||||
@@ -323,14 +332,19 @@ class Torrent(object):
|
||||
# Set the first/last priorities if needed
|
||||
self.set_prioritize_first_last(self.options["prioritize_first_last_pieces"])
|
||||
|
||||
def set_trackers(self, trackers):
|
||||
def set_trackers(self, trackers, reannounce=True):
|
||||
"""Sets trackers"""
|
||||
if trackers == None:
|
||||
trackers = []
|
||||
for value in self.handle.trackers():
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
if lt.version_major == 0 and lt.version_minor < 15:
|
||||
tracker = {}
|
||||
tracker["url"] = value.url
|
||||
tracker["tier"] = value.tier
|
||||
else:
|
||||
tracker = value
|
||||
# These unused lt 1.1.2 tracker datetime entries need to be None for rencode.
|
||||
tracker["min_announce"] = tracker["next_announce"] = None
|
||||
trackers.append(tracker)
|
||||
self.trackers = trackers
|
||||
self.tracker_host = None
|
||||
@@ -339,10 +353,13 @@ class Torrent(object):
|
||||
log.debug("Setting trackers for %s: %s", self.torrent_id, trackers)
|
||||
tracker_list = []
|
||||
|
||||
for tracker in trackers:
|
||||
for idx, tracker in enumerate(trackers):
|
||||
new_entry = lt.announce_entry(tracker["url"])
|
||||
new_entry.tier = tracker["tier"]
|
||||
tracker_list.append(new_entry)
|
||||
# These unused lt 1.1.2 tracker datetime entries need to be None for rencode.
|
||||
trackers[idx]["min_announce"] = trackers[idx]["next_announce"] = None
|
||||
|
||||
self.handle.replace_trackers(tracker_list)
|
||||
|
||||
# Print out the trackers
|
||||
@@ -350,7 +367,7 @@ class Torrent(object):
|
||||
# log.debug("tier: %s tracker: %s", t["tier"], t["url"])
|
||||
# Set the tracker list in the torrent object
|
||||
self.trackers = trackers
|
||||
if len(trackers) > 0:
|
||||
if len(trackers) > 0 and reannounce:
|
||||
# Force a reannounce if there is at least 1 tracker
|
||||
self.force_reannounce()
|
||||
|
||||
@@ -363,51 +380,56 @@ class Torrent(object):
|
||||
|
||||
def set_tracker_status(self, status):
|
||||
"""Sets the tracker status"""
|
||||
self.tracker_host = None
|
||||
self.tracker_status = self.get_tracker_host() + ": " + status
|
||||
|
||||
def update_state(self):
|
||||
"""Updates the state based on what libtorrent's state for the torrent is"""
|
||||
# Set the initial state based on the lt state
|
||||
LTSTATE = deluge.common.LT_TORRENT_STATE
|
||||
ltstate = int(self.handle.status().state)
|
||||
status = self.handle.status()
|
||||
ltstate = status.state
|
||||
|
||||
# Set self.state to the ltstate right away just incase we don't hit some
|
||||
# of the logic below
|
||||
if ltstate in LTSTATE:
|
||||
self.state = LTSTATE[ltstate]
|
||||
else:
|
||||
self.state = str(ltstate)
|
||||
# Set self.state to the ltstate right away just incase we don't hit some of the logic below
|
||||
old_state = self.state
|
||||
self.state = LTSTATE.get(int(ltstate), str(ltstate))
|
||||
|
||||
log.debug("set_state_based_on_ltstate: %s", deluge.common.LT_TORRENT_STATE[ltstate])
|
||||
log.debug("session.is_paused: %s", component.get("Core").session.is_paused())
|
||||
is_paused = self.handle.is_paused()
|
||||
is_auto_managed = self.handle.is_auto_managed()
|
||||
session_paused = component.get("Core").session.is_paused()
|
||||
|
||||
# First we check for an error from libtorrent, and set the state to that
|
||||
# if any occurred.
|
||||
if len(self.handle.status().error) > 0:
|
||||
if self.forced_error:
|
||||
self.state = "Error"
|
||||
self.set_status_message("Error: " + self.forced_error.error_message)
|
||||
elif status.error:
|
||||
# This is an error'd torrent
|
||||
self.state = "Error"
|
||||
self.set_status_message(self.handle.status().error)
|
||||
if self.handle.is_paused():
|
||||
self.set_status_message(status.error)
|
||||
if is_paused:
|
||||
self.handle.auto_managed(False)
|
||||
return
|
||||
|
||||
if ltstate == LTSTATE["Queued"] or ltstate == LTSTATE["Checking"]:
|
||||
if self.handle.is_paused():
|
||||
else:
|
||||
if is_paused and is_auto_managed and not session_paused:
|
||||
self.state = "Queued"
|
||||
elif is_paused or session_paused:
|
||||
self.state = "Paused"
|
||||
else:
|
||||
elif ltstate == LTSTATE["Queued"] or ltstate == LTSTATE["Checking"] or \
|
||||
ltstate == LTSTATE["Checking Resume Data"]:
|
||||
self.state = "Checking"
|
||||
return
|
||||
elif ltstate == LTSTATE["Downloading"] or ltstate == LTSTATE["Downloading Metadata"]:
|
||||
self.state = "Downloading"
|
||||
elif ltstate == LTSTATE["Finished"] or ltstate == LTSTATE["Seeding"]:
|
||||
self.state = "Seeding"
|
||||
elif ltstate == LTSTATE["Allocating"]:
|
||||
self.state = "Allocating"
|
||||
elif ltstate == LTSTATE["Downloading"] or ltstate == LTSTATE["Downloading Metadata"]:
|
||||
self.state = "Downloading"
|
||||
elif ltstate == LTSTATE["Finished"] or ltstate == LTSTATE["Seeding"]:
|
||||
self.state = "Seeding"
|
||||
elif ltstate == LTSTATE["Allocating"]:
|
||||
self.state = "Allocating"
|
||||
|
||||
if self.handle.is_paused() and self.handle.is_auto_managed() and not component.get("Core").session.is_paused():
|
||||
self.state = "Queued"
|
||||
elif component.get("Core").session.is_paused() or (self.handle.is_paused() and not self.handle.is_auto_managed()):
|
||||
self.state = "Paused"
|
||||
if self.state != old_state:
|
||||
log.debug("Using torrent state from lt: %s, auto_managed: %s, paused: %s, session_paused: %s",
|
||||
ltstate, is_auto_managed, is_paused, session_paused)
|
||||
log.debug("Torrent %s set from %s to %s: '%s'",
|
||||
self.torrent_id, old_state, self.state, self.statusmsg)
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(self.torrent_id, self.state))
|
||||
|
||||
def set_state(self, state):
|
||||
"""Accepts state strings, ie, "Paused", "Seeding", etc."""
|
||||
@@ -421,6 +443,37 @@ class Torrent(object):
|
||||
def set_status_message(self, message):
|
||||
self.statusmsg = message
|
||||
|
||||
def force_error_state(self, message, restart_to_resume=True):
|
||||
"""Forces the torrent into an error state.
|
||||
|
||||
For setting an error state not covered by libtorrent.
|
||||
|
||||
Args:
|
||||
message (str): The error status message.
|
||||
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
|
||||
session can resume.
|
||||
"""
|
||||
status = self.handle.status()
|
||||
self.handle.auto_managed(False)
|
||||
self.forced_error = TorrentError(message, status.paused, restart_to_resume)
|
||||
if not status.paused:
|
||||
self.handle.pause()
|
||||
self.update_state()
|
||||
|
||||
def clear_forced_error_state(self, update_state=True):
|
||||
if not self.forced_error:
|
||||
return
|
||||
|
||||
if self.forced_error.restart_to_resume:
|
||||
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.forced_error = None
|
||||
self.set_status_message("OK")
|
||||
if update_state:
|
||||
self.update_state()
|
||||
|
||||
def get_eta(self):
|
||||
"""Returns the ETA in seconds for this torrent"""
|
||||
if self.status == None:
|
||||
@@ -499,13 +552,15 @@ class Torrent(object):
|
||||
except UnicodeDecodeError:
|
||||
client = str(peer.client).decode("latin-1")
|
||||
|
||||
# Make country a proper string
|
||||
country = str()
|
||||
for c in peer.country:
|
||||
if not c.isalpha():
|
||||
country += " "
|
||||
else:
|
||||
country += c
|
||||
try:
|
||||
country = component.get("Core").geoip_instance.country_code_by_addr(peer.ip[0])
|
||||
except AttributeError:
|
||||
country = peer.country
|
||||
|
||||
try:
|
||||
country = "".join([char if char.isalpha() else " " for char in country])
|
||||
except TypeError:
|
||||
country = ""
|
||||
|
||||
ret.append({
|
||||
"client": client,
|
||||
@@ -523,10 +578,21 @@ class Torrent(object):
|
||||
"""Returns the torrents queue position"""
|
||||
return self.handle.queue_position()
|
||||
|
||||
def get_file_priorities(self):
|
||||
"""Return the file priorities"""
|
||||
if not self.handle.has_metadata():
|
||||
return []
|
||||
|
||||
if not self.options["file_priorities"]:
|
||||
# Ensure file_priorities option is populated.
|
||||
self.set_file_priorities([])
|
||||
|
||||
return self.options["file_priorities"]
|
||||
|
||||
def get_file_progress(self):
|
||||
"""Returns the file progress as a list of floats.. 0.0 -> 1.0"""
|
||||
if not self.handle.has_metadata():
|
||||
return 0.0
|
||||
return []
|
||||
|
||||
file_progress = self.handle.file_progress()
|
||||
ret = []
|
||||
@@ -535,6 +601,8 @@ class Torrent(object):
|
||||
ret.append(float(file_progress[i]) / float(f["size"]))
|
||||
except ZeroDivisionError:
|
||||
ret.append(0.0)
|
||||
except IndexError:
|
||||
return []
|
||||
|
||||
return ret
|
||||
|
||||
@@ -617,7 +685,6 @@ class Torrent(object):
|
||||
"compact": self.options["compact_allocation"],
|
||||
"distributed_copies": distributed_copies,
|
||||
"download_payload_rate": self.status.download_payload_rate,
|
||||
"file_priorities": self.options["file_priorities"],
|
||||
"hash": self.torrent_id,
|
||||
"is_auto_managed": self.options["auto_managed"],
|
||||
"is_finished": self.is_finished,
|
||||
@@ -716,6 +783,7 @@ class Torrent(object):
|
||||
fns = {
|
||||
"comment": ti_comment,
|
||||
"eta": self.get_eta,
|
||||
"file_priorities": self.get_file_priorities,
|
||||
"file_progress": self.get_file_progress,
|
||||
"files": self.get_files,
|
||||
"is_seed": self.handle.is_seed,
|
||||
@@ -776,6 +844,8 @@ class Torrent(object):
|
||||
|
||||
def pause(self):
|
||||
"""Pause this torrent"""
|
||||
if self.state == "Error":
|
||||
return False
|
||||
# Turn off auto-management so the torrent will not be unpaused by lt queueing
|
||||
self.handle.auto_managed(False)
|
||||
if self.handle.is_paused():
|
||||
@@ -799,7 +869,8 @@ class Torrent(object):
|
||||
|
||||
if self.handle.is_paused() and self.handle.is_auto_managed():
|
||||
log.debug("Torrent is being auto-managed, cannot resume!")
|
||||
return
|
||||
elif self.forced_error and self.forced_error.was_paused:
|
||||
log.debug("Skip resuming Error state torrent that was originally paused.")
|
||||
else:
|
||||
# Reset the status message just in case of resuming an Error'd torrent
|
||||
self.set_status_message("OK")
|
||||
@@ -823,6 +894,11 @@ class Torrent(object):
|
||||
|
||||
return True
|
||||
|
||||
if self.forced_error and not self.forced_error.restart_to_resume:
|
||||
self.clear_forced_error_state()
|
||||
elif self.state == "Error" and not self.forced_error:
|
||||
self.handle.clear_error()
|
||||
|
||||
def connect_peer(self, ip, port):
|
||||
"""adds manual peer"""
|
||||
try:
|
||||
@@ -835,27 +911,31 @@ class Torrent(object):
|
||||
def move_storage(self, dest):
|
||||
"""Move a torrent's storage location"""
|
||||
try:
|
||||
dest = unicode(dest, "utf-8")
|
||||
dest = unicode(dest, "utf-8")
|
||||
except TypeError:
|
||||
# String is already unicode
|
||||
pass
|
||||
|
||||
# String is already unicode
|
||||
pass
|
||||
|
||||
if not os.path.exists(dest):
|
||||
try:
|
||||
# Try to make the destination path if it doesn't exist
|
||||
os.makedirs(dest)
|
||||
except IOError, e:
|
||||
log.exception(e)
|
||||
log.error("Could not move storage for torrent %s since %s does not exist and could not create the directory.", self.torrent_id, dest_u)
|
||||
except OSError, ex:
|
||||
log.error("Could not move storage for torrent %s since %s does "
|
||||
"not exist and could not create the directory: %s",
|
||||
self.torrent_id, dest, ex)
|
||||
return False
|
||||
|
||||
kwargs = {}
|
||||
if deluge.common.VersionSplit(lt.version) >= deluge.common.VersionSplit("1.0.0.0"):
|
||||
kwargs['flags'] = 2 # dont_replace
|
||||
dest_bytes = dest.encode('utf-8')
|
||||
try:
|
||||
# libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise
|
||||
try:
|
||||
self.handle.move_storage(dest)
|
||||
self.handle.move_storage(dest, **kwargs)
|
||||
except TypeError:
|
||||
self.handle.move_storage(dest_bytes)
|
||||
self.handle.move_storage(dest_bytes, **kwargs)
|
||||
except Exception, e:
|
||||
log.error("Error calling libtorrent move_storage: %s" % e)
|
||||
return False
|
||||
@@ -865,8 +945,12 @@ class Torrent(object):
|
||||
def save_resume_data(self):
|
||||
"""Signals libtorrent to build resume data for this torrent, it gets
|
||||
returned in a libtorrent alert"""
|
||||
self.handle.save_resume_data()
|
||||
self.waiting_on_resume_data = True
|
||||
# Don't generate fastresume data if torrent is in a Deluge Error state.
|
||||
if self.forced_error:
|
||||
log.debug("Skipped creating resume_data while in Error state")
|
||||
else:
|
||||
self.handle.save_resume_data()
|
||||
self.waiting_on_resume_data = True
|
||||
|
||||
def on_metadata_received(self):
|
||||
if self.options["prioritize_first_last_pieces"]:
|
||||
@@ -886,7 +970,13 @@ class Torrent(object):
|
||||
md = lt.bdecode(self.torrent_info.metadata())
|
||||
torrent_file = {}
|
||||
torrent_file["info"] = md
|
||||
open(path, "wb").write(lt.bencode(torrent_file))
|
||||
with open(path, "wb") as _file:
|
||||
_file.write(lt.bencode(torrent_file))
|
||||
if self.config["copy_torrent_file"]:
|
||||
config_dir = self.config['torrentfiles_location']
|
||||
filepath = os.path.join(config_dir, self.filename)
|
||||
with open(filepath, "wb") as _file:
|
||||
_file.write(lt.bencode(torrent_file))
|
||||
except Exception, e:
|
||||
log.warning("Unable to save torrent file: %s", e)
|
||||
|
||||
@@ -923,16 +1013,27 @@ class Torrent(object):
|
||||
|
||||
def force_recheck(self):
|
||||
"""Forces a recheck of the torrents pieces"""
|
||||
paused = self.handle.is_paused()
|
||||
self.forcing_recheck = True
|
||||
if self.forced_error:
|
||||
self.forcing_recheck_paused = self.forced_error.was_paused
|
||||
self.clear_forced_error_state(update_state=False)
|
||||
else:
|
||||
self.forcing_recheck_paused = self.handle.is_paused()
|
||||
# Store trackers for paused torrents to prevent unwanted announce before pausing again.
|
||||
if self.forcing_recheck_paused:
|
||||
self.set_trackers(None, reannounce=False)
|
||||
self.handle.replace_trackers([])
|
||||
|
||||
try:
|
||||
self.handle.force_recheck()
|
||||
self.handle.resume()
|
||||
except Exception, e:
|
||||
log.debug("Unable to force recheck: %s", e)
|
||||
return False
|
||||
self.forcing_recheck = True
|
||||
self.forcing_recheck_paused = paused
|
||||
return True
|
||||
self.forcing_recheck = False
|
||||
if self.forcing_recheck_paused:
|
||||
self.set_trackers(torrent.trackers, reannounce=False)
|
||||
|
||||
return self.forcing_recheck
|
||||
|
||||
def rename_files(self, filenames):
|
||||
"""Renames files in the torrent. 'filenames' should be a list of
|
||||
@@ -981,4 +1082,3 @@ class Torrent(object):
|
||||
for key in self.prev_status.keys():
|
||||
if not self.rpcserver.is_session_valid(key):
|
||||
del self.prev_status[key]
|
||||
|
||||
|
@@ -55,7 +55,7 @@ from deluge.configmanager import ConfigManager, get_config_dir
|
||||
from deluge.core.torrent import Torrent
|
||||
from deluge.core.torrent import TorrentOptions
|
||||
import deluge.core.oldstateupgrader
|
||||
from deluge.common import utf8_encoded
|
||||
from deluge.common import utf8_encoded, decode_string
|
||||
|
||||
from deluge.log import LOG as log
|
||||
|
||||
@@ -111,10 +111,22 @@ class TorrentState:
|
||||
self.move_completed = move_completed
|
||||
self.move_completed_path = move_completed_path
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, TorrentState) and self.__dict__ == other.__dict__
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
class TorrentManagerState:
|
||||
def __init__(self):
|
||||
self.torrents = []
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, TorrentManagerState) and self.torrents == other.torrents
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
class TorrentManager(component.Component):
|
||||
"""
|
||||
TorrentManager contains a list of torrents in the current libtorrent
|
||||
@@ -148,12 +160,18 @@ class TorrentManager(component.Component):
|
||||
# self.num_resume_data used to save resume_data in bulk
|
||||
self.num_resume_data = 0
|
||||
|
||||
# Keep track of torrents finished but moving storage
|
||||
self.waiting_on_finish_moving = []
|
||||
|
||||
# Keeps track of resume data that needs to be saved to disk
|
||||
self.resume_data = {}
|
||||
|
||||
# Workaround to determine if TorrentAddedEvent is from state file
|
||||
self.session_started = False
|
||||
|
||||
# Keep the previous saved state
|
||||
self.prev_saved_state = None
|
||||
|
||||
# Register set functions
|
||||
self.config.register_set_function("max_connections_per_torrent",
|
||||
self.on_set_max_connections_per_torrent)
|
||||
@@ -181,6 +199,8 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_tracker_error)
|
||||
self.alerts.register_handler("storage_moved_alert",
|
||||
self.on_alert_storage_moved)
|
||||
self.alerts.register_handler("storage_moved_failed_alert",
|
||||
self.on_alert_storage_moved_failed)
|
||||
self.alerts.register_handler("torrent_resumed_alert",
|
||||
self.on_alert_torrent_resumed)
|
||||
self.alerts.register_handler("state_changed_alert",
|
||||
@@ -197,6 +217,12 @@ class TorrentManager(component.Component):
|
||||
self.on_alert_file_error)
|
||||
self.alerts.register_handler("file_completed_alert",
|
||||
self.on_alert_file_completed)
|
||||
self.alerts.register_handler("fastresume_rejected_alert",
|
||||
self.on_alert_fastresume_rejected)
|
||||
|
||||
# Define timers
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
|
||||
def start(self):
|
||||
# Get the pluginmanager reference
|
||||
@@ -205,14 +231,12 @@ class TorrentManager(component.Component):
|
||||
# Run the old state upgrader before loading state
|
||||
deluge.core.oldstateupgrader.OldStateUpgrader()
|
||||
|
||||
# Try to load the state from file
|
||||
# Try to load the state from file.
|
||||
self.load_state()
|
||||
|
||||
# Save the state every 5 minutes
|
||||
self.save_state_timer = LoopingCall(self.save_state)
|
||||
# Save the state and resume data every ~3 minutes.
|
||||
self.save_state_timer.start(200, False)
|
||||
self.save_resume_data_timer = LoopingCall(self.save_resume_data)
|
||||
self.save_resume_data_timer.start(190)
|
||||
self.save_resume_data_timer.start(190, False)
|
||||
|
||||
def stop(self):
|
||||
# Stop timers
|
||||
@@ -382,9 +406,14 @@ class TorrentManager(component.Component):
|
||||
# We have a torrent_info object or magnet uri so we're not loading from state.
|
||||
if torrent_info:
|
||||
add_torrent_id = str(torrent_info.info_hash())
|
||||
# If this torrent id is already in the session, merge any additional trackers.
|
||||
if add_torrent_id in self.get_torrent_list():
|
||||
# Torrent already exists just append any extra trackers.
|
||||
log.debug("Torrent (%s) exists, checking for trackers to add...", add_torrent_id)
|
||||
log.info("Merging trackers for torrent (%s) already in session...", add_torrent_id)
|
||||
# Don't merge trackers if either torrent has private flag set
|
||||
if torrent_info.priv() or self[add_torrent_id].get_status(["private"])["private"]:
|
||||
log.info("Merging trackers abandoned: Torrent has private flag set.")
|
||||
return
|
||||
|
||||
add_torrent_trackers = []
|
||||
for value in torrent_info.trackers():
|
||||
tracker = {}
|
||||
@@ -394,17 +423,18 @@ class TorrentManager(component.Component):
|
||||
|
||||
torrent_trackers = {}
|
||||
tracker_list = []
|
||||
for tracker in self[add_torrent_id].get_status(["trackers"])["trackers"]:
|
||||
for tracker in self[add_torrent_id].get_status(["trackers"])["trackers"]:
|
||||
torrent_trackers[(tracker["url"])] = tracker
|
||||
tracker_list.append(tracker)
|
||||
|
||||
added_tracker = False
|
||||
added_tracker = 0
|
||||
for tracker in add_torrent_trackers:
|
||||
if tracker['url'] not in torrent_trackers:
|
||||
tracker_list.append(tracker)
|
||||
added_tracker = True
|
||||
added_tracker += 1
|
||||
|
||||
if added_tracker:
|
||||
log.info("%s tracker(s) merged into torrent.", added_tracker)
|
||||
self[add_torrent_id].set_trackers(tracker_list)
|
||||
return
|
||||
|
||||
@@ -456,7 +486,8 @@ class TorrentManager(component.Component):
|
||||
handle = None
|
||||
try:
|
||||
if magnet:
|
||||
handle = lt.add_magnet_uri(self.session, utf8_encoded(magnet), add_torrent_params)
|
||||
magnet_uri = utf8_encoded(magnet.strip())
|
||||
handle = lt.add_magnet_uri(self.session, magnet_uri, add_torrent_params)
|
||||
else:
|
||||
handle = self.session.add_torrent(add_torrent_params)
|
||||
except RuntimeError, e:
|
||||
@@ -480,6 +511,10 @@ class TorrentManager(component.Component):
|
||||
|
||||
component.resume("AlertManager")
|
||||
|
||||
# Store the orignal resume_data, in case of errors.
|
||||
if resume_data:
|
||||
self.resume_data[torrent.torrent_id] = resume_data
|
||||
|
||||
# Resume the torrent if needed
|
||||
if not options["add_paused"]:
|
||||
torrent.resume()
|
||||
@@ -617,21 +652,24 @@ class TorrentManager(component.Component):
|
||||
|
||||
def load_state(self):
|
||||
"""Load the state of the TorrentManager from the torrents.state file"""
|
||||
state = TorrentManagerState()
|
||||
|
||||
try:
|
||||
log.debug("Opening torrent state file for load.")
|
||||
state_file = open(
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"), "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception, cPickle.UnpicklingError), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.state")
|
||||
log.debug("Opening torrent state file for load.")
|
||||
for _filepath in (filepath, filepath + ".bak"):
|
||||
try:
|
||||
state_file = open(_filepath, "rb")
|
||||
state = cPickle.load(state_file)
|
||||
state_file.close()
|
||||
except (EOFError, IOError, Exception, cPickle.UnpicklingError), e:
|
||||
log.warning("Unable to load state file: %s", e)
|
||||
state = TorrentManagerState()
|
||||
else:
|
||||
log.info("Successfully loaded state file: %s", _filepath)
|
||||
break
|
||||
|
||||
# Try to use an old state
|
||||
try:
|
||||
state_tmp = TorrentState()
|
||||
if dir(state.torrents[0]) != dir(state_tmp):
|
||||
if state.torrents and dir(state.torrents[0]) != dir(state_tmp):
|
||||
for attr in (set(dir(state_tmp)) - set(dir(state.torrents[0]))):
|
||||
for s in state.torrents:
|
||||
setattr(s, attr, getattr(state_tmp, attr, None))
|
||||
@@ -660,9 +698,14 @@ class TorrentManager(component.Component):
|
||||
state = TorrentManagerState()
|
||||
# Create the state for each Torrent and append to the list
|
||||
for torrent in self.torrents.values():
|
||||
paused = False
|
||||
if torrent.state == "Paused":
|
||||
if self.session.is_paused():
|
||||
paused = torrent.handle.is_paused()
|
||||
elif torrent.forced_error:
|
||||
paused = torrent.forced_error.was_paused
|
||||
elif torrent.state == "Paused":
|
||||
paused = True
|
||||
else:
|
||||
paused = False
|
||||
|
||||
torrent_state = TorrentState(
|
||||
torrent.torrent_id,
|
||||
@@ -691,27 +734,38 @@ class TorrentManager(component.Component):
|
||||
)
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
# If the state hasn't changed, no need to save it
|
||||
if self.prev_saved_state == state:
|
||||
return
|
||||
|
||||
# Pickle the TorrentManagerState object
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.state")
|
||||
filepath_tmp = filepath + ".tmp"
|
||||
filepath_bak = filepath + ".bak"
|
||||
|
||||
try:
|
||||
log.debug("Saving torrent state file.")
|
||||
state_file = open(os.path.join(get_config_dir(),
|
||||
"state", "torrents.state.new"), "wb")
|
||||
os.remove(filepath_bak)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
log.debug("Creating backup of state at: %s", filepath_bak)
|
||||
os.rename(filepath, filepath_bak)
|
||||
except OSError, ex:
|
||||
log.error("Unable to backup %s to %s: %s", filepath, filepath_bak, ex)
|
||||
try:
|
||||
log.info("Saving the state at: %s", filepath)
|
||||
state_file = open(filepath_tmp, "wb", 0)
|
||||
cPickle.dump(state, state_file)
|
||||
state_file.flush()
|
||||
os.fsync(state_file.fileno())
|
||||
state_file.close()
|
||||
except IOError, e:
|
||||
log.warning("Unable to save state file: %s", e)
|
||||
return True
|
||||
|
||||
# We have to move the 'torrents.state.new' file to 'torrents.state'
|
||||
try:
|
||||
shutil.move(
|
||||
os.path.join(get_config_dir(), "state", "torrents.state.new"),
|
||||
os.path.join(get_config_dir(), "state", "torrents.state"))
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
return True
|
||||
os.rename(filepath_tmp, filepath)
|
||||
self.prev_saved_state = state
|
||||
except IOError, ex:
|
||||
log.error("Unable to save %s: %s", filepath, ex)
|
||||
if os.path.isfile(filepath_bak):
|
||||
log.info("Restoring backup of state from: %s", filepath_bak)
|
||||
os.rename(filepath_bak, filepath)
|
||||
|
||||
# We return True so that the timer thread will continue
|
||||
return True
|
||||
@@ -731,15 +785,20 @@ class TorrentManager(component.Component):
|
||||
self.num_resume_data = len(torrent_ids)
|
||||
|
||||
def load_resume_data_file(self):
|
||||
resume_data = {}
|
||||
try:
|
||||
log.debug("Opening torrents fastresume file for load.")
|
||||
fastresume_file = open(os.path.join(get_config_dir(), "state",
|
||||
"torrents.fastresume"), "rb")
|
||||
resume_data = lt.bdecode(fastresume_file.read())
|
||||
fastresume_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
log.warning("Unable to load fastresume file: %s", e)
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
log.debug("Opening torrents fastresume file for load.")
|
||||
for _filepath in (filepath, filepath + ".bak"):
|
||||
try:
|
||||
fastresume_file = open(_filepath, "rb")
|
||||
resume_data = lt.bdecode(fastresume_file.read())
|
||||
fastresume_file.close()
|
||||
except (EOFError, IOError, Exception), e:
|
||||
if self.torrents:
|
||||
log.warning("Unable to load fastresume file: %s", e)
|
||||
resume_data = None
|
||||
else:
|
||||
log.info("Successfully loaded fastresume file: %s", _filepath)
|
||||
break
|
||||
|
||||
# If the libtorrent bdecode doesn't happen properly, it will return None
|
||||
# so we need to make sure we return a {}
|
||||
@@ -763,7 +822,9 @@ class TorrentManager(component.Component):
|
||||
if self.num_resume_data or not self.resume_data:
|
||||
return
|
||||
|
||||
path = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
filepath = os.path.join(get_config_dir(), "state", "torrents.fastresume")
|
||||
filepath_tmp = filepath + ".tmp"
|
||||
filepath_bak = filepath + ".bak"
|
||||
|
||||
# First step is to load the existing file and update the dictionary
|
||||
if resume_data is None:
|
||||
@@ -773,14 +834,27 @@ class TorrentManager(component.Component):
|
||||
self.resume_data = {}
|
||||
|
||||
try:
|
||||
log.debug("Saving fastresume file: %s", path)
|
||||
fastresume_file = open(path, "wb")
|
||||
os.remove(filepath_bak)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
log.debug("Creating backup of fastresume at: %s", filepath_bak)
|
||||
os.rename(filepath, filepath_bak)
|
||||
except OSError, ex:
|
||||
log.error("Unable to backup %s to %s: %s", filepath, filepath_bak, ex)
|
||||
try:
|
||||
log.info("Saving the fastresume at: %s", filepath)
|
||||
fastresume_file = open(filepath_tmp, "wb", 0)
|
||||
fastresume_file.write(lt.bencode(resume_data))
|
||||
fastresume_file.flush()
|
||||
os.fsync(fastresume_file.fileno())
|
||||
fastresume_file.close()
|
||||
except IOError:
|
||||
log.warning("Error trying to save fastresume file")
|
||||
os.rename(filepath_tmp, filepath)
|
||||
except IOError, ex:
|
||||
log.error("Unable to save %s: %s", filepath, ex)
|
||||
if os.path.isfile(filepath_bak):
|
||||
log.info("Restoring backup of fastresume from: %s", filepath_bak)
|
||||
os.rename(filepath_bak, filepath)
|
||||
|
||||
def remove_empty_folders(self, torrent_id, folder):
|
||||
"""
|
||||
@@ -887,19 +961,18 @@ class TorrentManager(component.Component):
|
||||
# that the torrent wasn't downloaded, but just added.
|
||||
total_download = torrent.get_status(["total_payload_download"])["total_payload_download"]
|
||||
|
||||
# Move completed download to completed folder if needed
|
||||
if not torrent.is_finished and total_download:
|
||||
move_path = None
|
||||
|
||||
if torrent.options["move_completed"]:
|
||||
move_path = torrent.options["move_completed_path"]
|
||||
if torrent.options["download_location"] != move_path:
|
||||
torrent.move_storage(move_path)
|
||||
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
torrent.is_finished = True
|
||||
torrent.update_state()
|
||||
if not torrent.is_finished and total_download:
|
||||
# Move completed download to completed folder if needed
|
||||
if torrent.options["move_completed"] and \
|
||||
torrent.options["download_location"] != torrent.options["move_completed_path"]:
|
||||
self.waiting_on_finish_moving.append(torrent_id)
|
||||
torrent.move_storage(torrent.options["move_completed_path"])
|
||||
else:
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
else:
|
||||
torrent.is_finished = True
|
||||
|
||||
# Torrent is no longer part of the queue
|
||||
try:
|
||||
@@ -924,10 +997,7 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
# Set the torrent state
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
# Don't save resume data for each torrent after self.stop() was called.
|
||||
# We save resume data in bulk in self.stop() in this case.
|
||||
@@ -951,12 +1021,13 @@ class TorrentManager(component.Component):
|
||||
torrent.forcing_recheck = False
|
||||
if torrent.forcing_recheck_paused:
|
||||
torrent.handle.pause()
|
||||
torrent.set_trackers(torrent.trackers, reannounce=False)
|
||||
|
||||
# Set the torrent state
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_tracker_reply(self, alert):
|
||||
log.debug("on_alert_tracker_reply: %s", alert.message().decode("utf8"))
|
||||
log.debug("on_alert_tracker_reply: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@@ -964,7 +1035,7 @@ class TorrentManager(component.Component):
|
||||
|
||||
# Set the tracker status for the torrent
|
||||
if alert.message() != "Got peers from DHT":
|
||||
torrent.set_tracker_status(_("Announce OK"))
|
||||
torrent.set_tracker_status("Announce OK")
|
||||
|
||||
# Check to see if we got any peer information from the tracker
|
||||
if alert.handle.status().num_complete == -1 or \
|
||||
@@ -980,7 +1051,7 @@ class TorrentManager(component.Component):
|
||||
return
|
||||
|
||||
# Set the tracker status for the torrent
|
||||
torrent.set_tracker_status(_("Announce Sent"))
|
||||
torrent.set_tracker_status("Announce Sent")
|
||||
|
||||
def on_alert_tracker_warning(self, alert):
|
||||
log.debug("on_alert_tracker_warning")
|
||||
@@ -988,28 +1059,55 @@ class TorrentManager(component.Component):
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
return
|
||||
tracker_status = '%s: %s' % (_("Warning"), str(alert.message()))
|
||||
tracker_status = '%s: %s' % ("Warning", decode_string(alert.message()))
|
||||
# Set the tracker status for the torrent
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
def on_alert_tracker_error(self, alert):
|
||||
log.debug("on_alert_tracker_error")
|
||||
"""Alert handler for libtorrent tracker_error_alert"""
|
||||
error_message = decode_string(alert.msg)
|
||||
# If alert.msg is empty then it's a '-1' code so fallback to a.e.message. Note that alert.msg
|
||||
# cannot be replaced by a.e.message because the code is included in the string (for non-'-1').
|
||||
if not error_message:
|
||||
error_message = decode_string(alert.error.message())
|
||||
log.debug("Tracker Error Alert: %s [%s]", decode_string(alert.message()), error_message)
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
tracker_status = "%s: %s" % (_("Error"), alert.msg)
|
||||
torrent.set_tracker_status(tracker_status)
|
||||
|
||||
torrent.set_tracker_status("Error: %s" % error_message)
|
||||
|
||||
def on_alert_storage_moved(self, alert):
|
||||
log.debug("on_alert_storage_moved")
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
torrent.set_save_path(os.path.normpath(alert.handle.save_path()))
|
||||
torrent.set_move_completed(False)
|
||||
|
||||
if torrent_id in self.waiting_on_finish_moving:
|
||||
self.waiting_on_finish_moving.remove(torrent_id)
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
def on_alert_storage_moved_failed(self, alert):
|
||||
"""Alert handler for libtorrent storage_moved_failed_alert"""
|
||||
log.debug("on_alert_storage_moved_failed: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
|
||||
log.error("Torrent %s, %s", torrent_id, decode_string(alert.message()))
|
||||
if torrent_id in self.waiting_on_finish_moving:
|
||||
self.waiting_on_finish_moving.remove(torrent_id)
|
||||
torrent.is_finished = True
|
||||
component.get("EventManager").emit(TorrentFinishedEvent(torrent_id))
|
||||
|
||||
def on_alert_torrent_resumed(self, alert):
|
||||
log.debug("on_alert_torrent_resumed")
|
||||
try:
|
||||
@@ -1017,11 +1115,7 @@ class TorrentManager(component.Component):
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
return
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
if torrent.state != old_state:
|
||||
# We need to emit a TorrentStateChangedEvent too
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
component.get("EventManager").emit(TorrentResumedEvent(torrent_id))
|
||||
|
||||
def on_alert_state_changed(self, alert):
|
||||
@@ -1032,7 +1126,6 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
|
||||
# Torrent may need to download data after checking.
|
||||
@@ -1040,10 +1133,6 @@ class TorrentManager(component.Component):
|
||||
torrent.is_finished = False
|
||||
self.queued_torrents.add(torrent_id)
|
||||
|
||||
# Only emit a state changed event if the state has actually changed
|
||||
if torrent.state != old_state:
|
||||
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
|
||||
|
||||
def on_alert_save_resume_data(self, alert):
|
||||
log.debug("on_alert_save_resume_data")
|
||||
try:
|
||||
@@ -1061,7 +1150,7 @@ class TorrentManager(component.Component):
|
||||
self.save_resume_data_file()
|
||||
|
||||
def on_alert_save_resume_data_failed(self, alert):
|
||||
log.debug("on_alert_save_resume_data_failed: %s", alert.message())
|
||||
log.debug("on_alert_save_resume_data_failed: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@@ -1072,10 +1161,29 @@ class TorrentManager(component.Component):
|
||||
|
||||
self.save_resume_data_file()
|
||||
|
||||
def on_alert_fastresume_rejected(self, alert):
|
||||
"""Alert handler for libtorrent fastresume_rejected_alert"""
|
||||
alert_msg = decode_string(alert.message())
|
||||
log.error("on_alert_fastresume_rejected: %s", alert_msg)
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
torrent = self.torrents[torrent_id]
|
||||
except (RuntimeError, KeyError):
|
||||
return
|
||||
|
||||
if alert.error.value() == 134:
|
||||
if not os.path.isdir(torrent.options["download_location"]):
|
||||
error_msg = "Unable to locate Download Folder!"
|
||||
else:
|
||||
error_msg = "Missing or invalid torrent data!"
|
||||
else:
|
||||
error_msg = "Problem with resume data: %s" % alert_msg.split(":", 1)[1].strip()
|
||||
|
||||
torrent.force_error_state(error_msg, restart_to_resume=True)
|
||||
|
||||
def on_alert_file_renamed(self, alert):
|
||||
log.debug("on_alert_file_renamed")
|
||||
log.debug("index: %s name: %s", alert.index, alert.name.decode("utf8"))
|
||||
log.debug("index: %s name: %s", alert.index, decode_string(alert.name))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
@@ -1113,7 +1221,7 @@ class TorrentManager(component.Component):
|
||||
torrent.on_metadata_received()
|
||||
|
||||
def on_alert_file_error(self, alert):
|
||||
log.debug("on_alert_file_error: %s", alert.message())
|
||||
log.debug("on_alert_file_error: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent = self.torrents[str(alert.handle.info_hash())]
|
||||
except:
|
||||
@@ -1121,7 +1229,7 @@ class TorrentManager(component.Component):
|
||||
torrent.update_state()
|
||||
|
||||
def on_alert_file_completed(self, alert):
|
||||
log.debug("file_completed_alert: %s", alert.message())
|
||||
log.debug("file_completed_alert: %s", decode_string(alert.message()))
|
||||
try:
|
||||
torrent_id = str(alert.handle.info_hash())
|
||||
except:
|
||||
|
BIN
deluge/data/icons/hicolor/16x16/apps/deluge-panel.png
Normal file
After Width: | Height: | Size: 722 B |
BIN
deluge/data/icons/hicolor/22x22/apps/deluge-panel.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
deluge/data/icons/hicolor/24x24/apps/deluge-panel.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 444 B |
Before Width: | Height: | Size: 408 B After Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 604 B After Width: | Height: | Size: 415 B |
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 431 B |
Before Width: | Height: | Size: 643 B After Width: | Height: | Size: 492 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 427 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 488 B After Width: | Height: | Size: 359 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 381 B |
Before Width: | Height: | Size: 836 B After Width: | Height: | Size: 582 B |
Before Width: | Height: | Size: 506 B After Width: | Height: | Size: 354 B |
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 403 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 554 B |
Before Width: | Height: | Size: 524 B After Width: | Height: | Size: 376 B |
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 470 B |
Before Width: | Height: | Size: 589 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 585 B After Width: | Height: | Size: 392 B |
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 286 B |
Before Width: | Height: | Size: 497 B After Width: | Height: | Size: 333 B |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 306 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 548 B |
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 486 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 458 B |
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 386 B |
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 460 B |
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 380 B |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 313 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 365 B |
Before Width: | Height: | Size: 600 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 625 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 614 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 366 B |
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 231 B |
Before Width: | Height: | Size: 453 B After Width: | Height: | Size: 300 B |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 316 B |
Before Width: | Height: | Size: 525 B After Width: | Height: | Size: 335 B |
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 483 B After Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 339 B |
Before Width: | Height: | Size: 439 B After Width: | Height: | Size: 305 B |
Before Width: | Height: | Size: 563 B After Width: | Height: | Size: 416 B |
Before Width: | Height: | Size: 529 B After Width: | Height: | Size: 403 B |
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 349 B |
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 353 B |
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 620 B After Width: | Height: | Size: 488 B |
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 582 B After Width: | Height: | Size: 442 B |
Before Width: | Height: | Size: 500 B After Width: | Height: | Size: 345 B |
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 344 B |
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 336 B |
Before Width: | Height: | Size: 592 B After Width: | Height: | Size: 424 B |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 355 B |
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 485 B |
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 498 B |
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 394 B |
Before Width: | Height: | Size: 474 B After Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 363 B |
Before Width: | Height: | Size: 694 B After Width: | Height: | Size: 460 B |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 637 B After Width: | Height: | Size: 446 B |
Before Width: | Height: | Size: 594 B After Width: | Height: | Size: 468 B |
Before Width: | Height: | Size: 545 B After Width: | Height: | Size: 363 B |
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 445 B |
Before Width: | Height: | Size: 490 B After Width: | Height: | Size: 320 B |