Compare commits
110 Commits
deluge-1.3
...
deluge-1.3
Author | SHA1 | Date | |
---|---|---|---|
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 |
4
AUTHORS
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
|
||||
|
98
ChangeLog
98
ChangeLog
@ -1,3 +1,101 @@
|
||||
=== 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
|
||||
|
@ -695,7 +695,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:
|
||||
@ -718,3 +718,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)]
|
||||
|
@ -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):
|
||||
|
@ -72,22 +72,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()
|
||||
@ -108,11 +116,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()
|
||||
@ -405,15 +414,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):
|
||||
@ -430,9 +442,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
|
||||
|
||||
@ -495,6 +507,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]
|
||||
|
@ -133,9 +133,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:
|
||||
|
@ -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)
|
||||
|
@ -337,11 +337,10 @@ 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)
|
||||
|
@ -493,10 +493,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()
|
||||
@ -509,7 +509,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())
|
||||
|
@ -98,6 +98,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.
|
||||
"""
|
||||
@ -141,6 +149,9 @@ class Torrent(object):
|
||||
# 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()
|
||||
|
||||
@ -185,11 +196,14 @@ class Torrent(object):
|
||||
# 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 +223,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 +247,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)
|
||||
|
||||
@ -323,14 +339,17 @@ 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
|
||||
trackers.append(tracker)
|
||||
self.trackers = trackers
|
||||
self.tracker_host = None
|
||||
@ -350,7 +369,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 +382,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 +445,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:
|
||||
@ -776,6 +831,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 +856,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 +881,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 +898,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, e:
|
||||
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'] = 1 # fail_if_exist
|
||||
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 +932,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"]:
|
||||
@ -923,16 +994,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 +1063,3 @@ class Torrent(object):
|
||||
for key in self.prev_status.keys():
|
||||
if not self.rpcserver.is_session_valid(key):
|
||||
del self.prev_status[key]
|
||||
|
||||
|
@ -202,6 +202,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
|
||||
@ -210,14 +216,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
|
||||
@ -391,7 +395,7 @@ class TorrentManager(component.Component):
|
||||
if add_torrent_id in self.get_torrent_list():
|
||||
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 self[add_torrent_id].get_status(["private"])["private"]:
|
||||
if torrent_info.priv() or self[add_torrent_id].get_status(["private"])["private"]:
|
||||
log.info("Merging trackers abandoned: Torrent has private flag set.")
|
||||
return
|
||||
|
||||
@ -491,6 +495,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()
|
||||
@ -628,21 +636,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))
|
||||
@ -671,9 +682,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,
|
||||
@ -703,26 +719,32 @@ class TorrentManager(component.Component):
|
||||
state.torrents.append(torrent_state)
|
||||
|
||||
# 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"))
|
||||
os.rename(filepath_tmp, filepath)
|
||||
except IOError:
|
||||
log.warning("Unable to save state file.")
|
||||
return True
|
||||
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
|
||||
@ -742,15 +764,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 {}
|
||||
@ -774,8 +801,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")
|
||||
path_tmp = path + ".tmp"
|
||||
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:
|
||||
@ -785,15 +813,27 @@ class TorrentManager(component.Component):
|
||||
self.resume_data = {}
|
||||
|
||||
try:
|
||||
log.debug("Saving fastresume file: %s", path)
|
||||
fastresume_file = open(path_tmp, "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()
|
||||
shutil.move(path_tmp, path)
|
||||
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):
|
||||
"""
|
||||
@ -936,10 +976,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.
|
||||
@ -963,6 +1000,7 @@ 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()
|
||||
@ -1005,13 +1043,19 @@ class TorrentManager(component.Component):
|
||||
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("%s: %s" % (_("Error"), error_message))
|
||||
|
||||
def on_alert_storage_moved(self, alert):
|
||||
log.debug("on_alert_storage_moved")
|
||||
@ -1037,6 +1081,7 @@ class TorrentManager(component.Component):
|
||||
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
|
||||
@ -1049,11 +1094,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):
|
||||
@ -1064,7 +1105,6 @@ class TorrentManager(component.Component):
|
||||
except:
|
||||
return
|
||||
|
||||
old_state = torrent.state
|
||||
torrent.update_state()
|
||||
|
||||
# Torrent may need to download data after checking.
|
||||
@ -1072,10 +1112,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:
|
||||
@ -1104,6 +1140,25 @@ 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")
|
||||
|
@ -146,7 +146,7 @@ def sanitise_filename(filename):
|
||||
log.warning("Potentially malicious server: trying to write to file '%s'" % filename)
|
||||
# Only use the basename
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
|
||||
filename = filename.strip()
|
||||
if filename.startswith(".") or ";" in filename or "|" in filename:
|
||||
# Dodgy server, log it
|
||||
@ -192,21 +192,42 @@ def download_file(url, filename, callback=None, headers=None, force_filename=Fal
|
||||
headers = {}
|
||||
headers["accept-encoding"] = "deflate, gzip, x-gzip"
|
||||
|
||||
# In twisted 13.1.0 the _parse() function was replaced by the _URI class
|
||||
if hasattr(client, '_parse'):
|
||||
# In Twisted 13.1.0 _parse() function replaced by _URI class.
|
||||
# In Twisted 15.0.0 _URI class renamed to URI.
|
||||
if hasattr(client, "_parse"):
|
||||
scheme, host, port, path = client._parse(url)
|
||||
else:
|
||||
from twisted.web.client import _URI
|
||||
uri = _URI.fromBytes(url)
|
||||
try:
|
||||
from twisted.web.client import _URI as URI
|
||||
except ImportError:
|
||||
from twisted.web.client import URI
|
||||
|
||||
uri = URI.fromBytes(url)
|
||||
scheme = uri.scheme
|
||||
host = uri.host
|
||||
port = uri.port
|
||||
path = uri.path
|
||||
|
||||
|
||||
factory = HTTPDownloader(url, filename, callback, headers, force_filename, allow_compression)
|
||||
if scheme == "https":
|
||||
from twisted.internet import ssl
|
||||
reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())
|
||||
# ClientTLSOptions in Twisted >= 14, see ticket #2765 for details on this addition.
|
||||
try:
|
||||
from twisted.internet._sslverify import ClientTLSOptions
|
||||
except ImportError:
|
||||
ctx_factory = ssl.ClientContextFactory()
|
||||
else:
|
||||
class TLSSNIContextFactory(ssl.ClientContextFactory):
|
||||
"""
|
||||
A custom context factory to add a server name for TLS connections.
|
||||
"""
|
||||
def getContext(self, hostname=None, port=None):
|
||||
ctx = ssl.ClientContextFactory.getContext(self)
|
||||
ClientTLSOptions(host, ctx)
|
||||
return ctx
|
||||
ctx_factory = TLSSNIContextFactory()
|
||||
|
||||
reactor.connectSSL(host, port, factory, ctx_factory)
|
||||
else:
|
||||
reactor.connectTCP(host, port, factory)
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2784
deluge/i18n/ast.po
2784
deluge/i18n/ast.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1888
deluge/i18n/sr.po
1888
deluge/i18n/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1696
deluge/i18n/vi.po
1696
deluge/i18n/vi.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2299
deluge/i18n/zh_HK.po
2299
deluge/i18n/zh_HK.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -86,7 +86,10 @@ def start_ui():
|
||||
help="Sets the log level to 'none', this is the same as `-L none`", action="store_true", default=False)
|
||||
|
||||
# Get the options and args from the OptionParser
|
||||
(options, args) = parser.parse_args()
|
||||
if deluge.common.windows_check():
|
||||
(options, args) = parser.parse_args(deluge.common.win32_unicode_argv()[1:])
|
||||
else:
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
# Setup the logger
|
||||
if options.quiet:
|
||||
@ -166,6 +169,9 @@ this should be an IP address", metavar="IFACE",
|
||||
help="Sets the log level to 'none', this is the same as `-L none`", action="store_true", default=False)
|
||||
parser.add_option("--profile", dest="profile", action="store_true", default=False,
|
||||
help="Profiles the daemon")
|
||||
parser.add_option("--read-only-config-keys", dest="read_only_config_keys",
|
||||
help="List of comma-separated config keys that will not be modified by set_config RPC.",
|
||||
action="store", type="str")
|
||||
|
||||
# Get the options and args from the OptionParser
|
||||
(options, args) = parser.parse_args()
|
||||
@ -173,6 +179,8 @@ this should be an IP address", metavar="IFACE",
|
||||
# Setup the logger
|
||||
if options.quiet:
|
||||
options.loglevel = "none"
|
||||
if options.loglevel:
|
||||
options.loglevel = options.loglevel.lower()
|
||||
if options.logfile:
|
||||
# Try to create the logfile's directory if it doesn't exist
|
||||
try:
|
||||
@ -204,7 +212,7 @@ this should be an IP address", metavar="IFACE",
|
||||
# If the donot daemonize is set, then we just skip the forking
|
||||
if not options.donot:
|
||||
# Windows check, we log to the config folder by default
|
||||
if deluge.common.windows_check() or deluge.common.osx_check():
|
||||
if deluge.common.windows_check():
|
||||
open_logfile()
|
||||
write_pidfile()
|
||||
else:
|
||||
|
@ -259,12 +259,13 @@ class Core(CorePluginBase):
|
||||
# Skip directories
|
||||
continue
|
||||
else:
|
||||
ext = os.path.splitext(filename)[1]
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if ext == ".torrent":
|
||||
magnet = False
|
||||
elif ext == ".magnet":
|
||||
magnet = True
|
||||
else:
|
||||
log.debug("File checked for auto-loading is invalid: %s", filename)
|
||||
continue
|
||||
try:
|
||||
filedump = self.load_torrent(filepath, magnet)
|
||||
|
@ -135,6 +135,8 @@ class Core(CorePluginBase):
|
||||
:rtype: Deferred
|
||||
"""
|
||||
|
||||
if not self.config["url"]:
|
||||
return
|
||||
|
||||
# Reset variables
|
||||
self.filename = None
|
||||
@ -425,12 +427,12 @@ class Core(CorePluginBase):
|
||||
|
||||
def pause_session(self):
|
||||
if not self.core.session.is_paused():
|
||||
self.core.session.pause()
|
||||
self.core.pause_all_torrents()
|
||||
self.need_to_resume_session = True
|
||||
else:
|
||||
self.need_to_resume_session = False
|
||||
|
||||
def resume_session(self, result):
|
||||
self.core.session.resume()
|
||||
self.core.resume_all_torrents()
|
||||
self.need_to_resume_session = False
|
||||
return result
|
||||
|
@ -138,7 +138,7 @@ class GtkUI(GtkPluginBase):
|
||||
|
||||
def _on_apply_prefs(self):
|
||||
config = {}
|
||||
config["url"] = self.glade.get_widget("entry_url").get_text()
|
||||
config["url"] = self.glade.get_widget("entry_url").get_text().strip()
|
||||
config["check_after_days"] = self.glade.get_widget("spin_check_days").get_value_as_int()
|
||||
config["load_on_start"] = self.glade.get_widget("chk_import_on_start").get_active()
|
||||
client.blocklist.set_config(config)
|
||||
|
@ -44,7 +44,7 @@ import deluge.component as component
|
||||
from deluge.configmanager import ConfigManager
|
||||
from deluge.core.rpcserver import export
|
||||
from deluge.event import DelugeEvent
|
||||
from deluge.common import utf8_encoded
|
||||
from deluge.common import utf8_encoded, windows_check
|
||||
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
@ -140,9 +140,15 @@ class Core(CorePluginBase):
|
||||
if command[EXECUTE_EVENT] == event:
|
||||
command = os.path.expandvars(command[EXECUTE_COMMAND])
|
||||
command = os.path.expanduser(command)
|
||||
|
||||
cmd_args = [torrent_id, torrent_name, save_path]
|
||||
if windows_check:
|
||||
# Escape ampersand on windows (see #2784)
|
||||
cmd_args = [arg.replace("&", "^^^&") for arg in cmd_args]
|
||||
|
||||
if os.path.isfile(command) and os.access(command, os.X_OK):
|
||||
log.debug("[execute] Running %s", command)
|
||||
d = getProcessOutputAndValue(command, (torrent_id, torrent_name, save_path), env=os.environ)
|
||||
log.debug("[execute] Running %s with args: %s", command, cmd_args)
|
||||
d = getProcessOutputAndValue(command, cmd_args, env=os.environ)
|
||||
d.addCallback(log_error, command)
|
||||
else:
|
||||
log.error("[execute] Execute script not found or not executable")
|
||||
|
@ -37,9 +37,10 @@
|
||||
#
|
||||
#
|
||||
|
||||
import errno
|
||||
import os
|
||||
|
||||
from twisted.internet.utils import getProcessValue
|
||||
from twisted.internet.utils import getProcessOutputAndValue
|
||||
|
||||
from deluge.log import LOG as log
|
||||
from deluge.plugins.pluginbase import CorePluginBase
|
||||
@ -142,40 +143,38 @@ class Core(CorePluginBase):
|
||||
if file_ext_sec and file_ext_sec + file_ext in EXTRACT_COMMANDS:
|
||||
file_ext = file_ext_sec + file_ext
|
||||
elif file_ext not in EXTRACT_COMMANDS or file_ext_sec == '.tar':
|
||||
log.warning("EXTRACTOR: Can't extract file with unknown file type: %s" % f["path"])
|
||||
log.debug("EXTRACTOR: Can't extract file with unknown file type: %s", f["path"])
|
||||
continue
|
||||
elif file_ext == ".rar" and "part" in file_ext_sec:
|
||||
part_num = file_ext_sec.split("part")[1]
|
||||
if part_num.isdigit() and int(part_num) != 1:
|
||||
log.debug("Skipping remaining multi-part rar files: %s", f["path"])
|
||||
continue
|
||||
|
||||
cmd = EXTRACT_COMMANDS[file_ext]
|
||||
|
||||
# Now that we have the cmd, lets run it to extract the files
|
||||
fpath = os.path.join(tid_status["save_path"], os.path.normpath(f["path"]))
|
||||
|
||||
# Get the destination path
|
||||
dest = os.path.normpath(self.config["extract_path"])
|
||||
if self.config["use_name_folder"]:
|
||||
name = tid_status["name"]
|
||||
dest = os.path.join(dest, name)
|
||||
dest = os.path.join(dest, tid_status["name"])
|
||||
|
||||
# Create the destination folder if it doesn't exist
|
||||
if not os.path.exists(dest):
|
||||
try:
|
||||
os.makedirs(dest)
|
||||
except Exception, e:
|
||||
log.error("EXTRACTOR: Error creating destination folder: %s", e)
|
||||
return
|
||||
try:
|
||||
os.makedirs(dest)
|
||||
except OSError, ex:
|
||||
if not (ex.errno == errno.EEXIST and os.path.isdir(dest)):
|
||||
log.error("EXTRACTOR: Error creating destination folder: %s", ex)
|
||||
break
|
||||
|
||||
def on_extract_success(result, torrent_id, fpath):
|
||||
# XXX: Emit an event
|
||||
log.info("EXTRACTOR: Extract successful: %s (%s)", fpath, torrent_id)
|
||||
def on_extract(result, torrent_id, fpath):
|
||||
# Check command exit code.
|
||||
if not result[2]:
|
||||
log.info("EXTRACTOR: Extract successful: %s (%s)", fpath, torrent_id)
|
||||
else:
|
||||
log.error("EXTRACTOR: Extract failed: %s (%s) %s", fpath, torrent_id, result[1])
|
||||
|
||||
def on_extract_failed(result, torrent_id, fpath):
|
||||
# XXX: Emit an event
|
||||
log.error("EXTRACTOR: Extract failed: %s (%s)", fpath, torrent_id)
|
||||
|
||||
# Run the command and add some callbacks
|
||||
log.debug("EXTRACTOR: Extracting %s with %s %s to %s", fpath, cmd[0], cmd[1], dest)
|
||||
d = getProcessValue(cmd[0], cmd[1].split() + [str(fpath)], {}, str(dest))
|
||||
d.addCallback(on_extract_success, torrent_id, fpath)
|
||||
d.addErrback(on_extract_failed, torrent_id, fpath)
|
||||
# Run the command and add callback.
|
||||
log.debug("EXTRACTOR: Extracting %s from %s with %s %s to %s", fpath, torrent_id, cmd[0], cmd[1], dest)
|
||||
d = getProcessOutputAndValue(cmd[0], cmd[1].split() + [str(fpath)], os.environ, str(dest))
|
||||
d.addCallback(on_extract, torrent_id, fpath)
|
||||
|
||||
@export
|
||||
def set_config(self, config):
|
||||
|
@ -42,7 +42,7 @@ from setuptools import setup
|
||||
__plugin_name__ = "Extractor"
|
||||
__author__ = "Andrew Resch"
|
||||
__author_email__ = "andrewresch@gmail.com"
|
||||
__version__ = "0.4"
|
||||
__version__ = "0.6"
|
||||
__url__ = "http://deluge-torrent.org"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Extract files upon torrent completion"
|
||||
|
@ -10,7 +10,6 @@
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="has_separator">False</property>
|
||||
<child internal-child="vbox">
|
||||
<widget class="GtkVBox" id="dialog-vbox1">
|
||||
<property name="visible">True</property>
|
||||
@ -155,7 +154,7 @@
|
||||
<widget class="GtkLabel" id="label6">
|
||||
<property name="visible">True</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Upload Slots: </property>
|
||||
<property name="label" translatable="yes">Upload Slots:</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="top_attach">2</property>
|
||||
@ -650,7 +649,6 @@
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="has_separator">False</property>
|
||||
<signal name="close" handler="on_label_cancel"/>
|
||||
<child internal-child="vbox">
|
||||
<widget class="GtkVBox" id="dialog-vbox2">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<glade-interface>
|
||||
<!-- interface-requires gtk+ 2.6 -->
|
||||
<!-- interface-requires gtk+ 2.16 -->
|
||||
<!-- interface-naming-policy toplevel-contextual -->
|
||||
<widget class="GtkWindow" id="window">
|
||||
<child>
|
||||
@ -179,7 +179,6 @@
|
||||
<widget class="GtkEntry" id="smtp_host">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="invisible_char">●</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@ -205,12 +204,10 @@
|
||||
<widget class="GtkSpinButton" id="smtp_port">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="max_length">65535</property>
|
||||
<property name="invisible_char">●</property>
|
||||
<property name="max_length">5</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="adjustment">25 1 65535 0 10 0</property>
|
||||
<property name="adjustment">25 1 65535 1 10 0</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="snap_to_ticks">True</property>
|
||||
<property name="numeric">True</property>
|
||||
</widget>
|
||||
<packing>
|
||||
@ -235,7 +232,6 @@
|
||||
<widget class="GtkEntry" id="smtp_user">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="invisible_char">●</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@ -259,7 +255,6 @@
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="visibility">False</property>
|
||||
<property name="invisible_char">●</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
@ -401,7 +396,6 @@
|
||||
<widget class="GtkEntry" id="smtp_from">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="invisible_char">●</property>
|
||||
</widget>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
|
@ -227,7 +227,7 @@ class GtkUiNotifications(CustomNotifications):
|
||||
return ''
|
||||
|
||||
def _on_torrent_finished_event_popup(self, torrent_id):
|
||||
d = client.core.get_torrent_status(torrent_id, ["name", "num_files"])
|
||||
d = client.core.get_torrent_status(torrent_id, ["name", "file_progress"])
|
||||
d.addCallback(self._on_torrent_finished_event_got_torrent_status)
|
||||
d.addErrback(self._on_torrent_finished_event_torrent_status_failure)
|
||||
return d
|
||||
@ -239,6 +239,7 @@ class GtkUiNotifications(CustomNotifications):
|
||||
log.debug("Handler for TorrentFinishedEvent GTKUI called. "
|
||||
"Got Torrent Status")
|
||||
title = _("Finished Torrent")
|
||||
torrent_status["num_files"] = torrent_status["file_progress"].count(1.0)
|
||||
message = _("The torrent \"%(name)s\" including %(num_files)i file(s) "
|
||||
"has finished downloading.") % torrent_status
|
||||
return title, message
|
||||
@ -253,7 +254,6 @@ class GtkUI(GtkPluginBase, GtkUiNotifications):
|
||||
"notifications-gtk.conf", DEFAULT_PREFS
|
||||
)
|
||||
self.glade = gtk.glade.XML(get_resource("config.glade"))
|
||||
self.glade.get_widget("smtp_port").set_value(25)
|
||||
self.prefs = self.glade.get_widget("prefs_box")
|
||||
self.prefs.show_all()
|
||||
|
||||
|
@ -129,7 +129,7 @@ class Core(CorePluginBase):
|
||||
for setting in CONTROLLED_SETTINGS:
|
||||
core_config.apply_set_functions(setting)
|
||||
# Resume the session if necessary
|
||||
component.get("Core").session.resume()
|
||||
component.get("Core").resume_all_torrents()
|
||||
|
||||
def do_schedule(self, timer=True):
|
||||
"""
|
||||
@ -153,10 +153,10 @@ class Core(CorePluginBase):
|
||||
settings.active_seeds = self.config["low_active_up"]
|
||||
session.set_settings(settings)
|
||||
# Resume the session if necessary
|
||||
component.get("Core").session.resume()
|
||||
component.get("Core").resume_all_torrents()
|
||||
elif state == "Red":
|
||||
# This is Red (Stop), so pause the libtorrent session
|
||||
component.get("Core").session.pause()
|
||||
component.get("Core").pause_all_torrents()
|
||||
|
||||
if state != self.state:
|
||||
# The state has changed since last update so we need to emit an event
|
||||
|
@ -77,20 +77,20 @@ class SchedulerSelectWidget(gtk.DrawingArea):
|
||||
|
||||
#redraw the whole thing
|
||||
def expose(self, widget, event):
|
||||
self.context = self.window.cairo_create()
|
||||
self.context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
|
||||
self.context.clip()
|
||||
context = self.window.cairo_create()
|
||||
context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height)
|
||||
context.clip()
|
||||
|
||||
width = self.window.get_size()[0]
|
||||
height = self.window.get_size()[1]
|
||||
|
||||
for y in xrange(7):
|
||||
for x in xrange(24):
|
||||
self.context.set_source_rgba(self.colors[self.button_state[x][y]][0], self.colors[self.button_state[x][y]][1], self.colors[self.button_state[x][y]][2], 0.7)
|
||||
self.context.rectangle(width*(6*x/145.0+1/145.0), height*(6*y/43.0+1/43.0), 5*width/145.0, 5*height/43.0)
|
||||
self.context.fill_preserve()
|
||||
self.context.set_source_rgba(0.5, 0.5, 0.5, 0.5)
|
||||
self.context.stroke()
|
||||
context.set_source_rgba(self.colors[self.button_state[x][y]][0], self.colors[self.button_state[x][y]][1], self.colors[self.button_state[x][y]][2], 0.7)
|
||||
context.rectangle(width*(6*x/145.0+1/145.0), height*(6*y/43.0+1/43.0), 5*width/145.0, 5*height/43.0)
|
||||
context.fill_preserve()
|
||||
context.set_source_rgba(0.5, 0.5, 0.5, 0.5)
|
||||
context.stroke()
|
||||
|
||||
#coordinates --> which box
|
||||
def get_point(self, event):
|
||||
@ -155,23 +155,26 @@ class GtkUI(GtkPluginBase):
|
||||
|
||||
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
|
||||
|
||||
self.status_item = component.get("StatusBar").add_item(
|
||||
self.statusbar = component.get("StatusBar")
|
||||
self.status_item = self.statusbar.add_item(
|
||||
image=get_resource("green.png"),
|
||||
text="",
|
||||
callback=self.on_status_item_clicked,
|
||||
tooltip="Scheduler")
|
||||
|
||||
def on_get_state(state):
|
||||
self.status_item.set_image_from_file(get_resource(state.lower() + ".png"))
|
||||
|
||||
self.state_deferred = client.scheduler.get_state().addCallback(on_get_state)
|
||||
def on_state_deferred(state):
|
||||
self.state = state
|
||||
self.on_scheduler_event(state)
|
||||
client.scheduler.get_state().addCallback(on_state_deferred)
|
||||
client.register_event_handler("SchedulerEvent", self.on_scheduler_event)
|
||||
|
||||
def disable(self):
|
||||
component.get("Preferences").remove_page(_("Scheduler"))
|
||||
# Remove status item
|
||||
component.get("StatusBar").remove_item(self.status_item)
|
||||
# Reset statusbar dict.
|
||||
self.statusbar.config_value_changed_dict["max_download_speed"] = self.statusbar._on_max_download_speed
|
||||
self.statusbar.config_value_changed_dict["max_upload_speed"] = self.statusbar._on_max_upload_speed
|
||||
# Remove statusbar item.
|
||||
self.statusbar.remove_item(self.status_item)
|
||||
del self.status_item
|
||||
|
||||
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs)
|
||||
@ -202,10 +205,30 @@ class GtkUI(GtkPluginBase):
|
||||
client.scheduler.get_config().addCallback(on_get_config)
|
||||
|
||||
def on_scheduler_event(self, state):
|
||||
def on_state_deferred(s):
|
||||
self.status_item.set_image_from_file(get_resource(state.lower() + ".png"))
|
||||
self.state = state
|
||||
self.status_item.set_image_from_file(get_resource(self.state.lower() + ".png"))
|
||||
if self.state == "Yellow":
|
||||
# Prevent func calls in Statusbar if the config changes.
|
||||
self.statusbar.config_value_changed_dict.pop("max_download_speed", None)
|
||||
self.statusbar.config_value_changed_dict.pop("max_upload_speed", None)
|
||||
try:
|
||||
self.statusbar._on_max_download_speed(self.spin_download.get_value())
|
||||
self.statusbar._on_max_upload_speed(self.spin_upload.get_value())
|
||||
except AttributeError:
|
||||
# Skip error due to Plugin being enabled before statusbar items created on startup.
|
||||
pass
|
||||
else:
|
||||
self.statusbar.config_value_changed_dict["max_download_speed"] = self.statusbar._on_max_download_speed
|
||||
self.statusbar.config_value_changed_dict["max_upload_speed"] = self.statusbar._on_max_upload_speed
|
||||
|
||||
self.state_deferred.addCallback(on_state_deferred)
|
||||
def update_config_values(config):
|
||||
try:
|
||||
self.statusbar._on_max_download_speed(config["max_download_speed"])
|
||||
self.statusbar._on_max_upload_speed(config["max_upload_speed"])
|
||||
except AttributeError:
|
||||
# Skip error due to Plugin being enabled before statusbar items created on startup.
|
||||
pass
|
||||
client.core.get_config_values(["max_download_speed", "max_upload_speed"]).addCallback(update_config_values)
|
||||
|
||||
def on_status_item_clicked(self, widget, event):
|
||||
component.get("Preferences").show("Scheduler")
|
||||
|
@ -41,7 +41,7 @@ from setuptools import setup
|
||||
__plugin_name__ = "Scheduler"
|
||||
__author__ = "Andrew Resch"
|
||||
__author_email__ = "andrewresch@gmail.com"
|
||||
__version__ = "0.2"
|
||||
__version__ = "0.3"
|
||||
__url__ = "http://deluge-torrent.org"
|
||||
__license__ = "GPLv3"
|
||||
__description__ = "Schedule limits on a per-hour per-day basis."
|
||||
|
@ -576,8 +576,6 @@ class Client(object):
|
||||
try:
|
||||
if deluge.common.windows_check():
|
||||
subprocess.Popen(["deluged", "--port=%s" % port, "--config=%s" % config])
|
||||
elif deluge.common.osx_check():
|
||||
subprocess.call(["nohup", "deluged", "--port=%s" % port, "--config=%s" % config])
|
||||
else:
|
||||
subprocess.call(["deluged", "--port=%s" % port, "--config=%s" % config])
|
||||
except OSError, e:
|
||||
|
@ -410,10 +410,11 @@ def get_localhost_auth():
|
||||
auth_file = deluge.configmanager.get_config_dir("auth")
|
||||
if os.path.exists(auth_file):
|
||||
for line in open(auth_file):
|
||||
if line.startswith("#"):
|
||||
# This is a comment line
|
||||
continue
|
||||
line = line.strip()
|
||||
if line.startswith("#") or not line:
|
||||
# This is a comment or blank line
|
||||
continue
|
||||
|
||||
try:
|
||||
lsplit = line.split(":")
|
||||
except Exception, e:
|
||||
|
@ -59,7 +59,7 @@ class Command(BaseCommand):
|
||||
|
||||
t_options = {}
|
||||
if options["path"]:
|
||||
t_options["download_location"] = os.path.expanduser(options["path"])
|
||||
t_options["download_location"] = os.path.abspath(os.path.expanduser(options["path"]))
|
||||
|
||||
def on_success(result):
|
||||
self.console.write("{!success!}Torrent added!")
|
||||
|
@ -34,8 +34,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from deluge.ui.console.main import BaseCommand
|
||||
import deluge.ui.console.colors as colors
|
||||
from deluge.ui.client import client
|
||||
@ -106,35 +104,33 @@ class Command(BaseCommand):
|
||||
return self._get_config(*args, **options)
|
||||
|
||||
def _get_config(self, *args, **options):
|
||||
deferred = defer.Deferred()
|
||||
config = component.get("CoreConfig")
|
||||
keys = config.keys()
|
||||
keys.sort()
|
||||
s = ""
|
||||
for key in keys:
|
||||
if args and key not in args:
|
||||
continue
|
||||
color = "{!white,black,bold!}"
|
||||
value = config[key]
|
||||
if type(value) in colors.type_color:
|
||||
color = colors.type_color[type(value)]
|
||||
def _on_get_config(config):
|
||||
keys = config.keys()
|
||||
keys.sort()
|
||||
s = ""
|
||||
for key in keys:
|
||||
if args and key not in args:
|
||||
continue
|
||||
color = "{!white,black,bold!}"
|
||||
value = config[key]
|
||||
if type(value) in colors.type_color:
|
||||
color = colors.type_color[type(value)]
|
||||
|
||||
# We need to format dicts for printing
|
||||
if isinstance(value, dict):
|
||||
import pprint
|
||||
value = pprint.pformat(value, 2, 80)
|
||||
new_value = []
|
||||
for line in value.splitlines():
|
||||
new_value.append("%s%s" % (color, line))
|
||||
value = "\n".join(new_value)
|
||||
# We need to format dicts for printing
|
||||
if isinstance(value, dict):
|
||||
import pprint
|
||||
value = pprint.pformat(value, 2, 80)
|
||||
new_value = []
|
||||
for line in value.splitlines():
|
||||
new_value.append("%s%s" % (color, line))
|
||||
value = "\n".join(new_value)
|
||||
|
||||
s += " %s: %s%s\n" % (key, color, value)
|
||||
s += " %s: %s%s\n" % (key, color, value)
|
||||
self.console.write(s)
|
||||
|
||||
self.console.write(s)
|
||||
return config
|
||||
return client.core.get_config().addCallback(_on_get_config)
|
||||
|
||||
def _set_config(self, *args, **options):
|
||||
deferred = defer.Deferred()
|
||||
config = component.get("CoreConfig")
|
||||
key = options["set"][0]
|
||||
val = simple_eval(options["set"][1] + " " .join(args))
|
||||
@ -152,11 +148,9 @@ class Command(BaseCommand):
|
||||
|
||||
def on_set_config(result):
|
||||
self.console.write("{!success!}Configuration value successfully updated.")
|
||||
deferred.callback(True)
|
||||
|
||||
self.console.write("Setting %s to %s.." % (key, val))
|
||||
client.core.set_config({key: val}).addCallback(on_set_config)
|
||||
return deferred
|
||||
return client.core.set_config({key: val}).addCallback(on_set_config)
|
||||
|
||||
def complete(self, text):
|
||||
return [ k for k in component.get("CoreConfig").keys() if k.startswith(text) ]
|
||||
|
@ -70,7 +70,8 @@ STATUS_KEYS = ["state",
|
||||
"is_seed",
|
||||
"is_finished",
|
||||
"active_time",
|
||||
"seeding_time"
|
||||
"seeding_time",
|
||||
"time_added"
|
||||
]
|
||||
|
||||
# Add filter specific state to torrent states
|
||||
|
@ -289,7 +289,7 @@ Please use commands from the command line, eg:\n
|
||||
if self.interactive:
|
||||
self.screen.add_line(line, not self.batch_write)
|
||||
else:
|
||||
print colors.strip_colors(line.encode("utf-8"))
|
||||
print colors.strip_colors(deluge.common.utf8_encoded(line))
|
||||
|
||||
def do_command(self, cmd):
|
||||
"""
|
||||
|
@ -221,7 +221,7 @@ COUNTRIES = {
|
||||
'SE': _('Sweden'),
|
||||
'CH': _('Switzerland'),
|
||||
'SY': _('Syrian Arab Republic'),
|
||||
'TW': _('Taiwan, Province of China'),
|
||||
'TW': _('Taiwan'),
|
||||
'TJ': _('Tajikistan'),
|
||||
'TZ': _('Tanzania, United Republic of'),
|
||||
'TH': _('Thailand'),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user