Merge lp:~e7appew/ubuntu/vivid/deluge/fix-set-trackers-cherry-picked into lp:ubuntu/vivid/deluge
- Vivid (15.04)
- fix-set-trackers-cherry-picked
- Merge into vivid
Proposed by
Carlos Maddela
Status: | Merged |
---|---|
Approved by: | Marc Deslauriers |
Approved revision: | 50 |
Merge reported by: | Marc Deslauriers |
Merged at revision: | not available |
Proposed branch: | lp:~e7appew/ubuntu/vivid/deluge/fix-set-trackers-cherry-picked |
Merge into: | lp:ubuntu/vivid/deluge |
Diff against target: |
1093 lines (+1039/-3) 7 files modified
.pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge/core/torrent.py (+984/-0) .pc/applied-patches (+1/-0) debian/changelog (+8/-0) debian/patches/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch (+38/-0) debian/patches/series (+1/-0) debian/patches/ubuntu.series (+1/-0) deluge/core/torrent.py (+6/-3) |
To merge this branch: | bzr merge lp:~e7appew/ubuntu/vivid/deluge/fix-set-trackers-cherry-picked |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marc Deslauriers | Approve | ||
Review via email: mp+271221@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory '.pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch' |
2 | === added directory '.pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge' |
3 | === added directory '.pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge/core' |
4 | === added file '.pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge/core/torrent.py' |
5 | --- .pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge/core/torrent.py 1970-01-01 00:00:00 +0000 |
6 | +++ .pc/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch/deluge/core/torrent.py 2015-09-16 02:26:39 +0000 |
7 | @@ -0,0 +1,984 @@ |
8 | +# |
9 | +# torrent.py |
10 | +# |
11 | +# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> |
12 | +# |
13 | +# Deluge is free software. |
14 | +# |
15 | +# You may redistribute it and/or modify it under the terms of the |
16 | +# GNU General Public License, as published by the Free Software |
17 | +# Foundation; either version 3 of the License, or (at your option) |
18 | +# any later version. |
19 | +# |
20 | +# deluge is distributed in the hope that it will be useful, |
21 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
23 | +# See the GNU General Public License for more details. |
24 | +# |
25 | +# You should have received a copy of the GNU General Public License |
26 | +# along with deluge. If not, write to: |
27 | +# The Free Software Foundation, Inc., |
28 | +# 51 Franklin Street, Fifth Floor |
29 | +# Boston, MA 02110-1301, USA. |
30 | +# |
31 | +# In addition, as a special exception, the copyright holders give |
32 | +# permission to link the code of portions of this program with the OpenSSL |
33 | +# library. |
34 | +# You must obey the GNU General Public License in all respects for all of |
35 | +# the code used other than OpenSSL. If you modify file(s) with this |
36 | +# exception, you may extend this exception to your version of the file(s), |
37 | +# but you are not obligated to do so. If you do not wish to do so, delete |
38 | +# this exception statement from your version. If you delete this exception |
39 | +# statement from all source files in the program, then also delete it here. |
40 | +# |
41 | + |
42 | +"""Internal Torrent class""" |
43 | + |
44 | +import os |
45 | +import time |
46 | +from urllib import unquote |
47 | +from urlparse import urlparse |
48 | + |
49 | +from deluge._libtorrent import lt |
50 | + |
51 | +import deluge.common |
52 | +import deluge.component as component |
53 | +from deluge.configmanager import ConfigManager, get_config_dir |
54 | +from deluge.log import LOG as log |
55 | +from deluge.event import * |
56 | + |
57 | +TORRENT_STATE = deluge.common.TORRENT_STATE |
58 | + |
59 | +def sanitize_filepath(filepath, folder=False): |
60 | + """ |
61 | + Returns a sanitized filepath to pass to libotorrent rename_file(). |
62 | + The filepath will have backslashes substituted along with whitespace |
63 | + padding and duplicate slashes stripped. If `folder` is True a trailing |
64 | + slash is appended to the returned filepath. |
65 | + """ |
66 | + def clean_filename(filename): |
67 | + filename = filename.strip() |
68 | + if filename.replace('.', '') == '': |
69 | + return '' |
70 | + return filename |
71 | + |
72 | + if '\\' in filepath or '/' in filepath: |
73 | + folderpath = filepath.replace('\\', '/').split('/') |
74 | + folderpath = [clean_filename(x) for x in folderpath] |
75 | + newfilepath = '/'.join(filter(None, folderpath)) |
76 | + else: |
77 | + newfilepath = clean_filename(filepath) |
78 | + |
79 | + if folder is True: |
80 | + return newfilepath + '/' |
81 | + else: |
82 | + return newfilepath |
83 | + |
84 | +class TorrentOptions(dict): |
85 | + def __init__(self): |
86 | + config = ConfigManager("core.conf").config |
87 | + options_conf_map = { |
88 | + "max_connections": "max_connections_per_torrent", |
89 | + "max_upload_slots": "max_upload_slots_per_torrent", |
90 | + "max_upload_speed": "max_upload_speed_per_torrent", |
91 | + "max_download_speed": "max_download_speed_per_torrent", |
92 | + "prioritize_first_last_pieces": "prioritize_first_last_pieces", |
93 | + "compact_allocation": "compact_allocation", |
94 | + "download_location": "download_location", |
95 | + "auto_managed": "auto_managed", |
96 | + "stop_at_ratio": "stop_seed_at_ratio", |
97 | + "stop_ratio": "stop_seed_ratio", |
98 | + "remove_at_ratio": "remove_seed_at_ratio", |
99 | + "move_completed": "move_completed", |
100 | + "move_completed_path": "move_completed_path", |
101 | + "add_paused": "add_paused", |
102 | + } |
103 | + for opt_k, conf_k in options_conf_map.iteritems(): |
104 | + self[opt_k] = config[conf_k] |
105 | + self["file_priorities"] = [] |
106 | + self["mapped_files"] = {} |
107 | + |
108 | +class Torrent(object): |
109 | + """Torrent holds information about torrents added to the libtorrent session. |
110 | + """ |
111 | + def __init__(self, handle, options, state=None, filename=None, magnet=None): |
112 | + log.debug("Creating torrent object %s", str(handle.info_hash())) |
113 | + # Get the core config |
114 | + self.config = ConfigManager("core.conf") |
115 | + |
116 | + self.rpcserver = component.get("RPCServer") |
117 | + |
118 | + # This dict holds previous status dicts returned for this torrent |
119 | + # We use this to return dicts that only contain changes from the previous |
120 | + # {session_id: status_dict, ...} |
121 | + self.prev_status = {} |
122 | + from twisted.internet.task import LoopingCall |
123 | + self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status) |
124 | + self.prev_status_cleanup_loop.start(10) |
125 | + |
126 | + # Set the libtorrent handle |
127 | + self.handle = handle |
128 | + # Set the torrent_id for this torrent |
129 | + self.torrent_id = str(handle.info_hash()) |
130 | + |
131 | + # Let's us know if we're waiting on a lt alert |
132 | + self.waiting_on_resume_data = False |
133 | + |
134 | + # Keep a list of file indexes we're waiting for file_rename alerts on |
135 | + # This also includes the old_folder and new_folder to know what signal to send |
136 | + # This is so we can send one folder_renamed signal instead of multiple |
137 | + # file_renamed signals. |
138 | + # [(old_folder, new_folder, [*indexes]), ...] |
139 | + self.waiting_on_folder_rename = [] |
140 | + |
141 | + # We store the filename just in case we need to make a copy of the torrentfile |
142 | + if not filename: |
143 | + # If no filename was provided, then just use the infohash |
144 | + filename = self.torrent_id |
145 | + |
146 | + self.filename = filename |
147 | + |
148 | + # Store the magnet uri used to add this torrent if available |
149 | + self.magnet = magnet |
150 | + |
151 | + # Holds status info so that we don't need to keep getting it from lt |
152 | + self.status = self.handle.status() |
153 | + |
154 | + try: |
155 | + self.torrent_info = self.handle.get_torrent_info() |
156 | + except RuntimeError: |
157 | + self.torrent_info = None |
158 | + |
159 | + # Default total_uploaded to 0, this may be changed by the state |
160 | + self.total_uploaded = 0 |
161 | + |
162 | + # Set the default options |
163 | + self.options = TorrentOptions() |
164 | + self.options.update(options) |
165 | + |
166 | + # We need to keep track if the torrent is finished in the state to prevent |
167 | + # some weird things on state load. |
168 | + self.is_finished = False |
169 | + |
170 | + # Load values from state if we have it |
171 | + if state: |
172 | + # This is for saving the total uploaded between sessions |
173 | + self.total_uploaded = state.total_uploaded |
174 | + # Set the trackers |
175 | + self.set_trackers(state.trackers) |
176 | + # Set the filename |
177 | + self.filename = state.filename |
178 | + self.is_finished = state.is_finished |
179 | + else: |
180 | + # Tracker list |
181 | + self.trackers = [] |
182 | + # Create a list of trackers |
183 | + for value in self.handle.trackers(): |
184 | + if lt.version_major == 0 and lt.version_minor < 15: |
185 | + tracker = {} |
186 | + tracker["url"] = value.url |
187 | + tracker["tier"] = value.tier |
188 | + else: |
189 | + tracker = value |
190 | + self.trackers.append(tracker) |
191 | + |
192 | + # Various torrent options |
193 | + self.handle.resolve_countries(True) |
194 | + |
195 | + self.set_options(self.options) |
196 | + |
197 | + # Status message holds error info about the torrent |
198 | + self.statusmsg = "OK" |
199 | + |
200 | + # The torrents state |
201 | + self.update_state() |
202 | + |
203 | + # The tracker status |
204 | + self.tracker_status = "" |
205 | + |
206 | + # This gets updated when get_tracker_host is called |
207 | + self.tracker_host = None |
208 | + |
209 | + if state: |
210 | + self.time_added = state.time_added |
211 | + else: |
212 | + self.time_added = time.time() |
213 | + |
214 | + # Keep track if we're forcing a recheck of the torrent so that we can |
215 | + # repause it after its done if necessary |
216 | + self.forcing_recheck = False |
217 | + self.forcing_recheck_paused = False |
218 | + |
219 | + log.debug("Torrent object created.") |
220 | + |
221 | + ## Options methods ## |
222 | + def set_options(self, options): |
223 | + OPTIONS_FUNCS = { |
224 | + # Functions used for setting options |
225 | + "auto_managed": self.set_auto_managed, |
226 | + "download_location": self.set_save_path, |
227 | + "file_priorities": self.set_file_priorities, |
228 | + "max_connections": self.handle.set_max_connections, |
229 | + "max_download_speed": self.set_max_download_speed, |
230 | + "max_upload_slots": self.handle.set_max_uploads, |
231 | + "max_upload_speed": self.set_max_upload_speed, |
232 | + "prioritize_first_last_pieces": self.set_prioritize_first_last |
233 | + } |
234 | + for (key, value) in options.items(): |
235 | + if OPTIONS_FUNCS.has_key(key): |
236 | + OPTIONS_FUNCS[key](value) |
237 | + |
238 | + self.options.update(options) |
239 | + |
240 | + def get_options(self): |
241 | + return self.options |
242 | + |
243 | + |
244 | + def set_max_connections(self, max_connections): |
245 | + self.options["max_connections"] = int(max_connections) |
246 | + self.handle.set_max_connections(max_connections) |
247 | + |
248 | + def set_max_upload_slots(self, max_slots): |
249 | + self.options["max_upload_slots"] = int(max_slots) |
250 | + self.handle.set_max_uploads(max_slots) |
251 | + |
252 | + def set_max_upload_speed(self, m_up_speed): |
253 | + self.options["max_upload_speed"] = m_up_speed |
254 | + if m_up_speed < 0: |
255 | + v = -1 |
256 | + else: |
257 | + v = int(m_up_speed * 1024) |
258 | + |
259 | + self.handle.set_upload_limit(v) |
260 | + |
261 | + def set_max_download_speed(self, m_down_speed): |
262 | + self.options["max_download_speed"] = m_down_speed |
263 | + if m_down_speed < 0: |
264 | + v = -1 |
265 | + else: |
266 | + v = int(m_down_speed * 1024) |
267 | + self.handle.set_download_limit(v) |
268 | + |
269 | + def set_prioritize_first_last(self, prioritize): |
270 | + if prioritize: |
271 | + if self.handle.has_metadata(): |
272 | + if self.handle.get_torrent_info().num_files() == 1: |
273 | + # We only do this if one file is in the torrent |
274 | + self.options["prioritize_first_last_pieces"] = prioritize |
275 | + priorities = [1] * self.handle.get_torrent_info().num_pieces() |
276 | + priorities[0] = 7 |
277 | + priorities[-1] = 7 |
278 | + self.handle.prioritize_pieces(priorities) |
279 | + |
280 | + def set_auto_managed(self, auto_managed): |
281 | + self.options["auto_managed"] = auto_managed |
282 | + if not (self.handle.is_paused() and not self.handle.is_auto_managed()): |
283 | + self.handle.auto_managed(auto_managed) |
284 | + self.update_state() |
285 | + |
286 | + def set_stop_ratio(self, stop_ratio): |
287 | + self.options["stop_ratio"] = stop_ratio |
288 | + |
289 | + def set_stop_at_ratio(self, stop_at_ratio): |
290 | + self.options["stop_at_ratio"] = stop_at_ratio |
291 | + |
292 | + def set_remove_at_ratio(self, remove_at_ratio): |
293 | + self.options["remove_at_ratio"] = remove_at_ratio |
294 | + |
295 | + def set_move_completed(self, move_completed): |
296 | + self.options["move_completed"] = move_completed |
297 | + |
298 | + def set_move_completed_path(self, move_completed_path): |
299 | + self.options["move_completed_path"] = move_completed_path |
300 | + |
301 | + def set_file_priorities(self, file_priorities): |
302 | + if len(file_priorities) != len(self.get_files()): |
303 | + log.debug("file_priorities len != num_files") |
304 | + self.options["file_priorities"] = self.handle.file_priorities() |
305 | + return |
306 | + |
307 | + if self.options["compact_allocation"]: |
308 | + log.debug("setting file priority with compact allocation does not work!") |
309 | + self.options["file_priorities"] = self.handle.file_priorities() |
310 | + return |
311 | + |
312 | + log.debug("setting %s's file priorities: %s", self.torrent_id, file_priorities) |
313 | + |
314 | + self.handle.prioritize_files(file_priorities) |
315 | + |
316 | + if 0 in self.options["file_priorities"]: |
317 | + # We have previously marked a file 'Do Not Download' |
318 | + # Check to see if we have changed any 0's to >0 and change state accordingly |
319 | + for index, priority in enumerate(self.options["file_priorities"]): |
320 | + if priority == 0 and file_priorities[index] > 0: |
321 | + # We have a changed 'Do Not Download' to a download priority |
322 | + self.is_finished = False |
323 | + self.update_state() |
324 | + break |
325 | + |
326 | + self.options["file_priorities"] = self.handle.file_priorities() |
327 | + if self.options["file_priorities"] != list(file_priorities): |
328 | + log.warning("File priorities were not set for this torrent") |
329 | + |
330 | + # Set the first/last priorities if needed |
331 | + self.set_prioritize_first_last(self.options["prioritize_first_last_pieces"]) |
332 | + |
333 | + def set_trackers(self, trackers): |
334 | + """Sets trackers""" |
335 | + if trackers == None: |
336 | + trackers = [] |
337 | + for value in self.handle.trackers(): |
338 | + tracker = {} |
339 | + tracker["url"] = value.url |
340 | + tracker["tier"] = value.tier |
341 | + trackers.append(tracker) |
342 | + self.trackers = trackers |
343 | + self.tracker_host = None |
344 | + return |
345 | + |
346 | + log.debug("Setting trackers for %s: %s", self.torrent_id, trackers) |
347 | + tracker_list = [] |
348 | + |
349 | + for tracker in trackers: |
350 | + new_entry = lt.announce_entry(tracker["url"]) |
351 | + new_entry.tier = tracker["tier"] |
352 | + tracker_list.append(new_entry) |
353 | + self.handle.replace_trackers(tracker_list) |
354 | + |
355 | + # Print out the trackers |
356 | + #for t in self.handle.trackers(): |
357 | + # log.debug("tier: %s tracker: %s", t["tier"], t["url"]) |
358 | + # Set the tracker list in the torrent object |
359 | + self.trackers = trackers |
360 | + if len(trackers) > 0: |
361 | + # Force a reannounce if there is at least 1 tracker |
362 | + self.force_reannounce() |
363 | + |
364 | + self.tracker_host = None |
365 | + |
366 | + ### End Options methods ### |
367 | + |
368 | + def set_save_path(self, save_path): |
369 | + self.options["download_location"] = save_path |
370 | + |
371 | + def set_tracker_status(self, status): |
372 | + """Sets the tracker status""" |
373 | + self.tracker_status = self.get_tracker_host() + ": " + status |
374 | + |
375 | + def update_state(self): |
376 | + """Updates the state based on what libtorrent's state for the torrent is""" |
377 | + # Set the initial state based on the lt state |
378 | + LTSTATE = deluge.common.LT_TORRENT_STATE |
379 | + ltstate = int(self.handle.status().state) |
380 | + |
381 | + # Set self.state to the ltstate right away just incase we don't hit some |
382 | + # of the logic below |
383 | + if ltstate in LTSTATE: |
384 | + self.state = LTSTATE[ltstate] |
385 | + else: |
386 | + self.state = str(ltstate) |
387 | + |
388 | + log.debug("set_state_based_on_ltstate: %s", deluge.common.LT_TORRENT_STATE[ltstate]) |
389 | + log.debug("session.is_paused: %s", component.get("Core").session.is_paused()) |
390 | + |
391 | + # First we check for an error from libtorrent, and set the state to that |
392 | + # if any occurred. |
393 | + if len(self.handle.status().error) > 0: |
394 | + # This is an error'd torrent |
395 | + self.state = "Error" |
396 | + self.set_status_message(self.handle.status().error) |
397 | + if self.handle.is_paused(): |
398 | + self.handle.auto_managed(False) |
399 | + return |
400 | + |
401 | + if ltstate == LTSTATE["Queued"] or ltstate == LTSTATE["Checking"]: |
402 | + if self.handle.is_paused(): |
403 | + self.state = "Paused" |
404 | + else: |
405 | + self.state = "Checking" |
406 | + return |
407 | + elif ltstate == LTSTATE["Downloading"] or ltstate == LTSTATE["Downloading Metadata"]: |
408 | + self.state = "Downloading" |
409 | + elif ltstate == LTSTATE["Finished"] or ltstate == LTSTATE["Seeding"]: |
410 | + self.state = "Seeding" |
411 | + elif ltstate == LTSTATE["Allocating"]: |
412 | + self.state = "Allocating" |
413 | + |
414 | + if self.handle.is_paused() and self.handle.is_auto_managed() and not component.get("Core").session.is_paused(): |
415 | + self.state = "Queued" |
416 | + elif component.get("Core").session.is_paused() or (self.handle.is_paused() and not self.handle.is_auto_managed()): |
417 | + self.state = "Paused" |
418 | + |
419 | + def set_state(self, state): |
420 | + """Accepts state strings, ie, "Paused", "Seeding", etc.""" |
421 | + if state not in TORRENT_STATE: |
422 | + log.debug("Trying to set an invalid state %s", state) |
423 | + return |
424 | + |
425 | + self.state = state |
426 | + return |
427 | + |
428 | + def set_status_message(self, message): |
429 | + self.statusmsg = message |
430 | + |
431 | + def get_eta(self): |
432 | + """Returns the ETA in seconds for this torrent""" |
433 | + if self.status == None: |
434 | + status = self.handle.status() |
435 | + else: |
436 | + status = self.status |
437 | + |
438 | + if self.is_finished and self.options["stop_at_ratio"]: |
439 | + # We're a seed, so calculate the time to the 'stop_share_ratio' |
440 | + if not status.upload_payload_rate: |
441 | + return 0 |
442 | + stop_ratio = self.options["stop_ratio"] |
443 | + return ((status.all_time_download * stop_ratio) - status.all_time_upload) / status.upload_payload_rate |
444 | + |
445 | + left = status.total_wanted - status.total_wanted_done |
446 | + |
447 | + if left <= 0 or status.download_payload_rate == 0: |
448 | + return 0 |
449 | + |
450 | + try: |
451 | + eta = left / status.download_payload_rate |
452 | + except ZeroDivisionError: |
453 | + eta = 0 |
454 | + |
455 | + return eta |
456 | + |
457 | + def get_ratio(self): |
458 | + """Returns the ratio for this torrent""" |
459 | + if self.status == None: |
460 | + status = self.handle.status() |
461 | + else: |
462 | + status = self.status |
463 | + |
464 | + if status.total_done > 0: |
465 | + # We use 'total_done' if the downloaded value is 0 |
466 | + downloaded = status.total_done |
467 | + else: |
468 | + # Return -1.0 to signify infinity |
469 | + return -1.0 |
470 | + |
471 | + return float(status.all_time_upload) / float(downloaded) |
472 | + |
473 | + def get_files(self): |
474 | + """Returns a list of files this torrent contains""" |
475 | + if self.torrent_info == None and self.handle.has_metadata(): |
476 | + torrent_info = self.handle.get_torrent_info() |
477 | + else: |
478 | + torrent_info = self.torrent_info |
479 | + |
480 | + if not torrent_info: |
481 | + return [] |
482 | + |
483 | + ret = [] |
484 | + files = torrent_info.files() |
485 | + for index, file in enumerate(files): |
486 | + ret.append({ |
487 | + 'index': index, |
488 | + # Make path separators consistent across platforms |
489 | + 'path': file.path.decode("utf8").replace('\\', '/'), |
490 | + 'size': file.size, |
491 | + 'offset': file.offset |
492 | + }) |
493 | + return ret |
494 | + |
495 | + def get_peers(self): |
496 | + """Returns a list of peers and various information about them""" |
497 | + ret = [] |
498 | + peers = self.handle.get_peer_info() |
499 | + |
500 | + for peer in peers: |
501 | + # We do not want to report peers that are half-connected |
502 | + if peer.flags & peer.connecting or peer.flags & peer.handshake: |
503 | + continue |
504 | + try: |
505 | + client = str(peer.client).decode("utf-8") |
506 | + except UnicodeDecodeError: |
507 | + client = str(peer.client).decode("latin-1") |
508 | + |
509 | + # Make country a proper string |
510 | + country = str() |
511 | + for c in peer.country: |
512 | + if not c.isalpha(): |
513 | + country += " " |
514 | + else: |
515 | + country += c |
516 | + |
517 | + ret.append({ |
518 | + "client": client, |
519 | + "country": country, |
520 | + "down_speed": peer.payload_down_speed, |
521 | + "ip": "%s:%s" % (peer.ip[0], peer.ip[1]), |
522 | + "progress": peer.progress, |
523 | + "seed": peer.flags & peer.seed, |
524 | + "up_speed": peer.payload_up_speed, |
525 | + }) |
526 | + |
527 | + return ret |
528 | + |
529 | + def get_queue_position(self): |
530 | + """Returns the torrents queue position""" |
531 | + return self.handle.queue_position() |
532 | + |
533 | + def get_file_progress(self): |
534 | + """Returns the file progress as a list of floats.. 0.0 -> 1.0""" |
535 | + if not self.handle.has_metadata(): |
536 | + return 0.0 |
537 | + |
538 | + file_progress = self.handle.file_progress() |
539 | + ret = [] |
540 | + for i,f in enumerate(self.get_files()): |
541 | + try: |
542 | + ret.append(float(file_progress[i]) / float(f["size"])) |
543 | + except ZeroDivisionError: |
544 | + ret.append(0.0) |
545 | + |
546 | + return ret |
547 | + |
548 | + def get_tracker_host(self): |
549 | + """Returns just the hostname of the currently connected tracker |
550 | + if no tracker is connected, it uses the 1st tracker.""" |
551 | + if self.tracker_host: |
552 | + return self.tracker_host |
553 | + |
554 | + if not self.status: |
555 | + self.status = self.handle.status() |
556 | + |
557 | + tracker = self.status.current_tracker |
558 | + if not tracker and self.trackers: |
559 | + tracker = self.trackers[0]["url"] |
560 | + |
561 | + if tracker: |
562 | + url = urlparse(tracker.replace("udp://", "http://")) |
563 | + if hasattr(url, "hostname"): |
564 | + host = (url.hostname or 'DHT') |
565 | + # Check if hostname is an IP address and just return it if that's the case |
566 | + import socket |
567 | + try: |
568 | + socket.inet_aton(host) |
569 | + except socket.error: |
570 | + pass |
571 | + else: |
572 | + # This is an IP address because an exception wasn't raised |
573 | + return url.hostname |
574 | + |
575 | + parts = host.split(".") |
576 | + if len(parts) > 2: |
577 | + if parts[-2] in ("co", "com", "net", "org") or parts[-1] in ("uk"): |
578 | + host = ".".join(parts[-3:]) |
579 | + else: |
580 | + host = ".".join(parts[-2:]) |
581 | + self.tracker_host = host |
582 | + return host |
583 | + return "" |
584 | + |
585 | + def get_status(self, keys, diff=False): |
586 | + """ |
587 | + Returns the status of the torrent based on the keys provided |
588 | + |
589 | + :param keys: the keys to get the status on |
590 | + :type keys: list of str |
591 | + :param diff: if True, will return a diff of the changes since the last |
592 | + call to get_status based on the session_id |
593 | + :type diff: bool |
594 | + |
595 | + :returns: a dictionary of the status keys and their values |
596 | + :rtype: dict |
597 | + |
598 | + """ |
599 | + |
600 | + # Create the full dictionary |
601 | + self.status = self.handle.status() |
602 | + if self.handle.has_metadata(): |
603 | + self.torrent_info = self.handle.get_torrent_info() |
604 | + |
605 | + # Adjust progress to be 0-100 value |
606 | + progress = self.status.progress * 100 |
607 | + |
608 | + # Adjust status.distributed_copies to return a non-negative value |
609 | + distributed_copies = self.status.distributed_copies |
610 | + if distributed_copies < 0: |
611 | + distributed_copies = 0.0 |
612 | + |
613 | + # Calculate the seeds:peers ratio |
614 | + if self.status.num_incomplete == 0: |
615 | + # Use -1.0 to signify infinity |
616 | + seeds_peers_ratio = -1.0 |
617 | + else: |
618 | + seeds_peers_ratio = self.status.num_complete / float(self.status.num_incomplete) |
619 | + |
620 | + #if you add a key here->add it to core.py STATUS_KEYS too. |
621 | + full_status = { |
622 | + "active_time": self.status.active_time, |
623 | + "all_time_download": self.status.all_time_download, |
624 | + "compact": self.options["compact_allocation"], |
625 | + "distributed_copies": distributed_copies, |
626 | + "download_payload_rate": self.status.download_payload_rate, |
627 | + "file_priorities": self.options["file_priorities"], |
628 | + "hash": self.torrent_id, |
629 | + "is_auto_managed": self.options["auto_managed"], |
630 | + "is_finished": self.is_finished, |
631 | + "max_connections": self.options["max_connections"], |
632 | + "max_download_speed": self.options["max_download_speed"], |
633 | + "max_upload_slots": self.options["max_upload_slots"], |
634 | + "max_upload_speed": self.options["max_upload_speed"], |
635 | + "message": self.statusmsg, |
636 | + "move_on_completed_path": self.options["move_completed_path"], |
637 | + "move_on_completed": self.options["move_completed"], |
638 | + "move_completed_path": self.options["move_completed_path"], |
639 | + "move_completed": self.options["move_completed"], |
640 | + "next_announce": self.status.next_announce.seconds, |
641 | + "num_peers": self.status.num_peers - self.status.num_seeds, |
642 | + "num_seeds": self.status.num_seeds, |
643 | + "paused": self.status.paused, |
644 | + "prioritize_first_last": self.options["prioritize_first_last_pieces"], |
645 | + "progress": progress, |
646 | + "remove_at_ratio": self.options["remove_at_ratio"], |
647 | + "save_path": self.options["download_location"], |
648 | + "seeding_time": self.status.seeding_time, |
649 | + "seeds_peers_ratio": seeds_peers_ratio, |
650 | + "seed_rank": self.status.seed_rank, |
651 | + "state": self.state, |
652 | + "stop_at_ratio": self.options["stop_at_ratio"], |
653 | + "stop_ratio": self.options["stop_ratio"], |
654 | + "time_added": self.time_added, |
655 | + "total_done": self.status.total_done, |
656 | + "total_payload_download": self.status.total_payload_download, |
657 | + "total_payload_upload": self.status.total_payload_upload, |
658 | + "total_peers": self.status.num_incomplete, |
659 | + "total_seeds": self.status.num_complete, |
660 | + "total_uploaded": self.status.all_time_upload, |
661 | + "total_wanted": self.status.total_wanted, |
662 | + "tracker": self.status.current_tracker, |
663 | + "trackers": self.trackers, |
664 | + "tracker_status": self.tracker_status, |
665 | + "upload_payload_rate": self.status.upload_payload_rate |
666 | + } |
667 | + |
668 | + def ti_comment(): |
669 | + if self.handle.has_metadata(): |
670 | + try: |
671 | + return self.torrent_info.comment().decode("utf8", "ignore") |
672 | + except UnicodeDecodeError: |
673 | + return self.torrent_info.comment() |
674 | + return "" |
675 | + |
676 | + def ti_name(): |
677 | + if self.handle.has_metadata(): |
678 | + name = self.torrent_info.file_at(0).path.replace("\\", "/", 1).split("/", 1)[0] |
679 | + if not name: |
680 | + name = self.torrent_info.name() |
681 | + try: |
682 | + return name.decode("utf8", "ignore") |
683 | + except UnicodeDecodeError: |
684 | + return name |
685 | + |
686 | + elif self.magnet: |
687 | + try: |
688 | + keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')]) |
689 | + name = keys.get('dn') |
690 | + if not name: |
691 | + return self.torrent_id |
692 | + name = unquote(name).replace('+', ' ') |
693 | + try: |
694 | + return name.decode("utf8", "ignore") |
695 | + except UnicodeDecodeError: |
696 | + return name |
697 | + except: |
698 | + pass |
699 | + |
700 | + return self.torrent_id |
701 | + |
702 | + def ti_priv(): |
703 | + if self.handle.has_metadata(): |
704 | + return self.torrent_info.priv() |
705 | + return False |
706 | + def ti_total_size(): |
707 | + if self.handle.has_metadata(): |
708 | + return self.torrent_info.total_size() |
709 | + return 0 |
710 | + def ti_num_files(): |
711 | + if self.handle.has_metadata(): |
712 | + return self.torrent_info.num_files() |
713 | + return 0 |
714 | + def ti_num_pieces(): |
715 | + if self.handle.has_metadata(): |
716 | + return self.torrent_info.num_pieces() |
717 | + return 0 |
718 | + def ti_piece_length(): |
719 | + if self.handle.has_metadata(): |
720 | + return self.torrent_info.piece_length() |
721 | + return 0 |
722 | + |
723 | + fns = { |
724 | + "comment": ti_comment, |
725 | + "eta": self.get_eta, |
726 | + "file_progress": self.get_file_progress, |
727 | + "files": self.get_files, |
728 | + "is_seed": self.handle.is_seed, |
729 | + "name": ti_name, |
730 | + "num_files": ti_num_files, |
731 | + "num_pieces": ti_num_pieces, |
732 | + "peers": self.get_peers, |
733 | + "piece_length": ti_piece_length, |
734 | + "private": ti_priv, |
735 | + "queue": self.handle.queue_position, |
736 | + "ratio": self.get_ratio, |
737 | + "total_size": ti_total_size, |
738 | + "tracker_host": self.get_tracker_host, |
739 | + } |
740 | + |
741 | + # Create the desired status dictionary and return it |
742 | + status_dict = {} |
743 | + |
744 | + if len(keys) == 0: |
745 | + status_dict = full_status |
746 | + for key in fns: |
747 | + status_dict[key] = fns[key]() |
748 | + else: |
749 | + for key in keys: |
750 | + if key in full_status: |
751 | + status_dict[key] = full_status[key] |
752 | + elif key in fns: |
753 | + status_dict[key] = fns[key]() |
754 | + |
755 | + session_id = self.rpcserver.get_session_id() |
756 | + if diff: |
757 | + if session_id in self.prev_status: |
758 | + # We have a previous status dict, so lets make a diff |
759 | + status_diff = {} |
760 | + for key, value in status_dict.items(): |
761 | + if key in self.prev_status[session_id]: |
762 | + if value != self.prev_status[session_id][key]: |
763 | + status_diff[key] = value |
764 | + else: |
765 | + status_diff[key] = value |
766 | + |
767 | + self.prev_status[session_id] = status_dict |
768 | + return status_diff |
769 | + |
770 | + self.prev_status[session_id] = status_dict |
771 | + return status_dict |
772 | + |
773 | + return status_dict |
774 | + |
775 | + def apply_options(self): |
776 | + """Applies the per-torrent options that are set.""" |
777 | + self.handle.set_max_connections(self.max_connections) |
778 | + self.handle.set_max_uploads(self.max_upload_slots) |
779 | + self.handle.set_upload_limit(int(self.max_upload_speed * 1024)) |
780 | + self.handle.set_download_limit(int(self.max_download_speed * 1024)) |
781 | + self.handle.prioritize_files(self.file_priorities) |
782 | + self.handle.resolve_countries(True) |
783 | + |
784 | + def pause(self): |
785 | + """Pause this torrent""" |
786 | + # Turn off auto-management so the torrent will not be unpaused by lt queueing |
787 | + self.handle.auto_managed(False) |
788 | + if self.handle.is_paused(): |
789 | + # This torrent was probably paused due to being auto managed by lt |
790 | + # Since we turned auto_managed off, we should update the state which should |
791 | + # show it as 'Paused'. We need to emit a torrent_paused signal because |
792 | + # the torrent_paused alert from libtorrent will not be generated. |
793 | + self.update_state() |
794 | + component.get("EventManager").emit(TorrentStateChangedEvent(self.torrent_id, "Paused")) |
795 | + else: |
796 | + try: |
797 | + self.handle.pause() |
798 | + except Exception, e: |
799 | + log.debug("Unable to pause torrent: %s", e) |
800 | + return False |
801 | + |
802 | + return True |
803 | + |
804 | + def resume(self): |
805 | + """Resumes this torrent""" |
806 | + |
807 | + if self.handle.is_paused() and self.handle.is_auto_managed(): |
808 | + log.debug("Torrent is being auto-managed, cannot resume!") |
809 | + return |
810 | + else: |
811 | + # Reset the status message just in case of resuming an Error'd torrent |
812 | + self.set_status_message("OK") |
813 | + |
814 | + if self.handle.is_finished(): |
815 | + # If the torrent has already reached it's 'stop_seed_ratio' then do not do anything |
816 | + if self.options["stop_at_ratio"]: |
817 | + if self.get_ratio() >= self.options["stop_ratio"]: |
818 | + #XXX: This should just be returned in the RPC Response, no event |
819 | + #self.signals.emit_event("torrent_resume_at_stop_ratio") |
820 | + return |
821 | + |
822 | + if self.options["auto_managed"]: |
823 | + # This torrent is to be auto-managed by lt queueing |
824 | + self.handle.auto_managed(True) |
825 | + |
826 | + try: |
827 | + self.handle.resume() |
828 | + except: |
829 | + pass |
830 | + |
831 | + return True |
832 | + |
833 | + def connect_peer(self, ip, port): |
834 | + """adds manual peer""" |
835 | + try: |
836 | + self.handle.connect_peer((ip, int(port)), 0) |
837 | + except Exception, e: |
838 | + log.debug("Unable to connect to peer: %s", e) |
839 | + return False |
840 | + return True |
841 | + |
842 | + def move_storage(self, dest): |
843 | + """Move a torrent's storage location""" |
844 | + try: |
845 | + dest = unicode(dest, "utf-8") |
846 | + except TypeError: |
847 | + # String is already unicode |
848 | + pass |
849 | + |
850 | + if not os.path.exists(dest): |
851 | + try: |
852 | + # Try to make the destination path if it doesn't exist |
853 | + os.makedirs(dest) |
854 | + except IOError, e: |
855 | + log.exception(e) |
856 | + 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) |
857 | + return False |
858 | + |
859 | + dest_bytes = dest.encode('utf-8') |
860 | + try: |
861 | + # libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise |
862 | + try: |
863 | + self.handle.move_storage(dest) |
864 | + except TypeError: |
865 | + self.handle.move_storage(dest_bytes) |
866 | + except Exception, e: |
867 | + log.error("Error calling libtorrent move_storage: %s" % e) |
868 | + return False |
869 | + |
870 | + return True |
871 | + |
872 | + def save_resume_data(self): |
873 | + """Signals libtorrent to build resume data for this torrent, it gets |
874 | + returned in a libtorrent alert""" |
875 | + self.handle.save_resume_data() |
876 | + self.waiting_on_resume_data = True |
877 | + |
878 | + def on_metadata_received(self): |
879 | + if self.options["prioritize_first_last_pieces"]: |
880 | + self.set_prioritize_first_last(True) |
881 | + self.write_torrentfile() |
882 | + |
883 | + def write_torrentfile(self): |
884 | + """Writes the torrent file""" |
885 | + path = "%s/%s.torrent" % ( |
886 | + os.path.join(get_config_dir(), "state"), |
887 | + self.torrent_id) |
888 | + log.debug("Writing torrent file: %s", path) |
889 | + try: |
890 | + self.torrent_info = self.handle.get_torrent_info() |
891 | + # Regenerate the file priorities |
892 | + self.set_file_priorities([]) |
893 | + md = lt.bdecode(self.torrent_info.metadata()) |
894 | + torrent_file = {} |
895 | + torrent_file["info"] = md |
896 | + open(path, "wb").write(lt.bencode(torrent_file)) |
897 | + except Exception, e: |
898 | + log.warning("Unable to save torrent file: %s", e) |
899 | + |
900 | + def delete_torrentfile(self): |
901 | + """Deletes the .torrent file in the state""" |
902 | + path = "%s/%s.torrent" % ( |
903 | + os.path.join(get_config_dir(), "state"), |
904 | + self.torrent_id) |
905 | + log.debug("Deleting torrent file: %s", path) |
906 | + try: |
907 | + os.remove(path) |
908 | + except Exception, e: |
909 | + log.warning("Unable to delete the torrent file: %s", e) |
910 | + |
911 | + def force_reannounce(self): |
912 | + """Force a tracker reannounce""" |
913 | + try: |
914 | + self.handle.force_reannounce() |
915 | + except Exception, e: |
916 | + log.debug("Unable to force reannounce: %s", e) |
917 | + return False |
918 | + |
919 | + return True |
920 | + |
921 | + def scrape_tracker(self): |
922 | + """Scrape the tracker""" |
923 | + try: |
924 | + self.handle.scrape_tracker() |
925 | + except Exception, e: |
926 | + log.debug("Unable to scrape tracker: %s", e) |
927 | + return False |
928 | + |
929 | + return True |
930 | + |
931 | + def force_recheck(self): |
932 | + """Forces a recheck of the torrents pieces""" |
933 | + paused = self.handle.is_paused() |
934 | + try: |
935 | + self.handle.force_recheck() |
936 | + self.handle.resume() |
937 | + except Exception, e: |
938 | + log.debug("Unable to force recheck: %s", e) |
939 | + return False |
940 | + self.forcing_recheck = True |
941 | + self.forcing_recheck_paused = paused |
942 | + return True |
943 | + |
944 | + def rename_files(self, filenames): |
945 | + """Renames files in the torrent. 'filenames' should be a list of |
946 | + (index, filename) pairs.""" |
947 | + for index, filename in filenames: |
948 | + # Make sure filename is a unicode object |
949 | + try: |
950 | + filename = unicode(filename, "utf-8") |
951 | + except TypeError: |
952 | + pass |
953 | + filename = sanitize_filepath(filename) |
954 | + # libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise |
955 | + try: |
956 | + self.handle.rename_file(index, filename) |
957 | + except TypeError: |
958 | + self.handle.rename_file(index, filename.encode("utf-8")) |
959 | + |
960 | + def rename_folder(self, folder, new_folder): |
961 | + """Renames a folder within a torrent. This basically does a file rename |
962 | + on all of the folders children.""" |
963 | + log.debug("attempting to rename folder: %s to %s", folder, new_folder) |
964 | + if len(new_folder) < 1: |
965 | + log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder) |
966 | + return |
967 | + |
968 | + new_folder = sanitize_filepath(new_folder, folder=True) |
969 | + |
970 | + wait_on_folder = (folder, new_folder, []) |
971 | + for f in self.get_files(): |
972 | + if f["path"].startswith(folder): |
973 | + # Keep a list of filerenames we're waiting on |
974 | + wait_on_folder[2].append(f["index"]) |
975 | + new_path = f["path"].replace(folder, new_folder, 1) |
976 | + try: |
977 | + self.handle.rename_file(f["index"], new_path) |
978 | + except TypeError: |
979 | + self.handle.rename_file(f["index"], new_path.encode("utf-8")) |
980 | + self.waiting_on_folder_rename.append(wait_on_folder) |
981 | + |
982 | + def cleanup_prev_status(self): |
983 | + """ |
984 | + This method gets called to check the validity of the keys in the prev_status |
985 | + dict. If the key is no longer valid, the dict will be deleted. |
986 | + |
987 | + """ |
988 | + for key in self.prev_status.keys(): |
989 | + if not self.rpcserver.is_session_valid(key): |
990 | + del self.prev_status[key] |
991 | + |
992 | |
993 | === modified file '.pc/applied-patches' |
994 | --- .pc/applied-patches 2014-08-30 20:20:56 +0000 |
995 | +++ .pc/applied-patches 2015-09-16 02:26:39 +0000 |
996 | @@ -1,2 +1,3 @@ |
997 | new_release_check.patch |
998 | +Fix-AttributeError-in-set_trackers-with-lt-1.0.patch |
999 | ubuntu_indicator.patch |
1000 | |
1001 | === modified file 'debian/changelog' |
1002 | --- debian/changelog 2015-04-05 10:59:46 +0000 |
1003 | +++ debian/changelog 2015-09-16 02:26:39 +0000 |
1004 | @@ -1,3 +1,11 @@ |
1005 | +deluge (1.3.11-0ubuntu2.1) vivid; urgency=medium |
1006 | + |
1007 | + * Non-maintainer upload. |
1008 | + * Fix AttributeError in set_trackers with lt 1.0, cherry-picked from |
1009 | + upstream (LP: #1487704) (#2223). |
1010 | + |
1011 | + -- Carlos Maddela <maddela@labyrinth.net.au> Wed, 16 Sep 2015 11:55:31 +1000 |
1012 | + |
1013 | deluge (1.3.11-0ubuntu2) vivid; urgency=medium |
1014 | |
1015 | * Import changes from Debian Testing: |
1016 | |
1017 | === added file 'debian/patches/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch' |
1018 | --- debian/patches/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch 1970-01-01 00:00:00 +0000 |
1019 | +++ debian/patches/Fix-AttributeError-in-set_trackers-with-lt-1.0.patch 2015-09-16 02:26:39 +0000 |
1020 | @@ -0,0 +1,38 @@ |
1021 | +From: Calum Lind <calumlind+deluge@gmail.com> |
1022 | +Date: Sat, 22 Aug 2015 15:31:26 +0100 |
1023 | +Subject: Fix AttributeError in set_trackers with lt 1.0 |
1024 | + |
1025 | +Description: Fix AttributeError in set_trackers with lt 1.0 |
1026 | + When using a more recent version of libtorrent, set_trackers() |
1027 | + will result in an "AttributeError: 'dict' object has no attribute 'url'" |
1028 | + exception if the trackers parameter is None, just like upstream bug #2223. |
1029 | +Author: Calum Lind <calumlind+deluge@gmail.com> |
1030 | +Origin: upstream, http://git.deluge-torrent.org/deluge/commit/?h=1.3-stable&id=acf4fc4193d |
1031 | +Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/deluge/+bug/1487704 |
1032 | +Last-Update: 2015-08-22 |
1033 | +--- |
1034 | +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ |
1035 | +--- |
1036 | + deluge/core/torrent.py | 9 ++++++--- |
1037 | + 1 file changed, 6 insertions(+), 3 deletions(-) |
1038 | + |
1039 | +diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py |
1040 | +index 7ca35e1..9003d8b 100644 |
1041 | +--- a/deluge/core/torrent.py |
1042 | ++++ b/deluge/core/torrent.py |
1043 | +@@ -328,9 +328,12 @@ class Torrent(object): |
1044 | + if trackers == None: |
1045 | + trackers = [] |
1046 | + for value in self.handle.trackers(): |
1047 | +- tracker = {} |
1048 | +- tracker["url"] = value.url |
1049 | +- tracker["tier"] = value.tier |
1050 | ++ if lt.version_major == 0 and lt.version_minor < 15: |
1051 | ++ tracker = {} |
1052 | ++ tracker["url"] = value.url |
1053 | ++ tracker["tier"] = value.tier |
1054 | ++ else: |
1055 | ++ tracker = value |
1056 | + trackers.append(tracker) |
1057 | + self.trackers = trackers |
1058 | + self.tracker_host = None |
1059 | |
1060 | === modified file 'debian/patches/series' |
1061 | --- debian/patches/series 2014-12-14 20:16:28 +0000 |
1062 | +++ debian/patches/series 2015-09-16 02:26:39 +0000 |
1063 | @@ -1,1 +1,2 @@ |
1064 | new_release_check.patch |
1065 | +Fix-AttributeError-in-set_trackers-with-lt-1.0.patch |
1066 | |
1067 | === modified file 'debian/patches/ubuntu.series' |
1068 | --- debian/patches/ubuntu.series 2014-08-30 20:20:56 +0000 |
1069 | +++ debian/patches/ubuntu.series 2015-09-16 02:26:39 +0000 |
1070 | @@ -1,2 +1,3 @@ |
1071 | new_release_check.patch |
1072 | +Fix-AttributeError-in-set_trackers-with-lt-1.0.patch |
1073 | ubuntu_indicator.patch |
1074 | |
1075 | === modified file 'deluge/core/torrent.py' |
1076 | --- deluge/core/torrent.py 2014-10-05 11:27:46 +0000 |
1077 | +++ deluge/core/torrent.py 2015-09-16 02:26:39 +0000 |
1078 | @@ -328,9 +328,12 @@ |
1079 | if trackers == None: |
1080 | trackers = [] |
1081 | for value in self.handle.trackers(): |
1082 | - tracker = {} |
1083 | - tracker["url"] = value.url |
1084 | - tracker["tier"] = value.tier |
1085 | + if lt.version_major == 0 and lt.version_minor < 15: |
1086 | + tracker = {} |
1087 | + tracker["url"] = value.url |
1088 | + tracker["tier"] = value.tier |
1089 | + else: |
1090 | + tracker = value |
1091 | trackers.append(tracker) |
1092 | self.trackers = trackers |
1093 | self.tracker_host = None |
Looks good, ack. Uploaded for processing by the SRU team. Thanks!