Ticket #2087: torrent.py

File torrent.py, 35.9 KB (added by Raziel.Azrael, 13 years ago)

A simple fix in the Torrent.py file when a torrent is loaded

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