1 # Written by Arno Bakker
2 # see LICENSE.txt for license information
3 """ Contains a snapshot of the state of the Download at a specific point in time. """
7 from traceback import print_exc,print_stack
9 from BaseLib.Core.simpledefs import *
10 from BaseLib.Core.defaults import *
11 from BaseLib.Core.exceptions import *
12 from BaseLib.Core.Base import *
13 from BaseLib.Core.DecentralizedTracking.repex import REPEX_SWARMCACHE_SIZE
17 class DownloadState(Serializable):
19 Contains a snapshot of the state of the Download at a specific
20 point in time. Using a snapshot instead of providing live data and
21 protecting access via locking should be faster.
23 cf. libtorrent torrent_status
25 def __init__(self,download,status,error,progress,stats=None,filepieceranges=None,logmsgs=None,coopdl_helpers=[],coopdl_coordinator=None,peerid=None,videoinfo=None,swarmcache=None):
26 """ Internal constructor.
27 @param download The Download this state belongs too.
28 @param status The status of the Download (DLSTATUS_*)
29 @param progress The general progress of the Download.
30 @param stats The BT engine statistics for the Download.
31 @param filepieceranges The range of pieces that we are interested in.
32 The get_pieces_complete() returns only completeness information about
33 this range. This is used for playing a video in a multi-torrent file.
34 @param logmsgs A list of messages from the BT engine which may be of
36 # Raynor Vliegendhart, TODO: documentation of DownloadState seems incomplete?
37 # RePEX: @param swarmcache The latest SwarmCache known by Download. This
38 # cache will be used when the download is not running.
39 # RePEX TODO: instead of being passed the latest SwarmCache, DownloadState could
40 # also query it from Download? Perhaps add get_swarmcache to Download(Impl)?
42 self.download = download
43 self.filepieceranges = filepieceranges # NEED CONC CONTROL IF selected_files RUNTIME SETABLE
44 self.logmsgs = logmsgs
45 self.coopdl_helpers = coopdl_helpers
46 self.coopdl_coordinator = coopdl_coordinator
48 # RePEX: stored swarmcache from Download and store current time
49 if swarmcache is not None:
50 self.swarmcache = dict(swarmcache)
52 self.swarmcache = None
53 self.time = time.time()
56 # No info available yet from download engine
57 self.error = error # readonly access
58 self.progress = progress
59 if self.error is not None:
60 self.status = DLSTATUS_STOPPED_ON_ERROR
64 elif error is not None:
65 self.error = error # readonly access
66 self.progress = 0.0 # really want old progress
67 self.status = DLSTATUS_STOPPED_ON_ERROR
69 elif status is not None and status != DLSTATUS_REPEXING:
70 # For HASHCHECKING and WAITING4HASHCHECK
73 if self.status == DLSTATUS_WAITING4HASHCHECK:
76 self.progress = stats['frac']
79 # Copy info from stats
81 self.progress = stats['frac']
82 if stats['frac'] == 1.0:
83 self.status = DLSTATUS_SEEDING
85 self.status = DLSTATUS_DOWNLOADING
86 #print >>sys.stderr,"STATS IS",stats
88 # Safe to store the stats dict. The stats dict is created per
89 # invocation of the BT1Download returned statsfunc and contains no
95 statsobj = self.stats['stats']
96 if self.filepieceranges is None:
97 self.haveslice = statsobj.have # is copy of network engine list
99 # Show only pieces complete for the selected ranges of files
101 for t,tl,f in self.filepieceranges:
105 #print >>sys.stderr,"DownloadState: get_pieces_complete",totalpieces
107 haveslice = [False] * totalpieces
110 for t,tl,f in self.filepieceranges:
111 for piece in range(t,tl):
112 haveslice[index] = statsobj.have[piece]
113 if haveall and haveslice[index] == False:
116 self.haveslice = haveslice
117 if haveall and len(self.filepieceranges) > 0:
118 # we have all pieces of the selected files
119 self.status = DLSTATUS_SEEDING
122 # RePEX: REPEXING status overrides SEEDING/DOWNLOADING status.
123 if status is not None and status == DLSTATUS_REPEXING:
124 self.status = DLSTATUS_REPEXING
127 def get_download(self):
128 """ Returns the Download object of which this is the state """
131 def get_progress(self):
132 """ The general progress of the Download as a percentage. When status is
133 * DLSTATUS_HASHCHECKING it is the percentage of already downloaded
134 content checked for integrity.
135 * DLSTATUS_DOWNLOADING/SEEDING it is the percentage downloaded.
136 @return Progress as a float (0..1).
140 def get_status(self):
141 """ Returns the status of the torrent.
142 @return DLSTATUS_* """
146 """ Returns the Exception that caused the download to be moved to
147 DLSTATUS_STOPPED_ON_ERROR status.
155 def get_current_speed(self,direct):
157 Returns the current up or download speed.
158 @return The speed in KB/s, as float.
160 if self.stats is None:
163 return self.stats['up']/1024.0
165 return self.stats['down']/1024.0
167 def get_total_transferred(self,direct):
169 Returns the total amount of up or downloaded bytes.
170 @return The amount in bytes.
172 if self.stats is None:
174 # self.stats: BitTornado.BT1.DownloaderFeedback.py (return from gather method)
175 # self.stats["stats"]: BitTornado.BT1.Statistics.py (Statistics_Response instance)
177 return self.stats['stats'].upTotal
179 return self.stats['stats'].downTotal
183 Returns the estimated time to finish of download.
184 @return The time in ?, as ?.
186 if self.stats is None:
189 return self.stats['time']
191 def get_num_peers(self):
193 Returns the download's number of active connections. This is used
194 to see if there is any progress when non-fatal errors have occured
195 (e.g. tracker timeout).
198 if self.stats is None:
201 # Determine if we need statsobj to be requested, same as for spew
202 statsobj = self.stats['stats']
203 return statsobj.numSeeds+statsobj.numPeers
205 def get_num_seeds_peers(self):
207 Returns the sum of the number of seeds and peers. This function
208 works only if the Download.set_state_callback() /
209 Session.set_download_states_callback() was called with the getpeerlist
210 parameter set to True, otherwise returns (None,None)
211 @return A tuple (num seeds, num peers)
213 if self.stats is None or self.stats['spew'] is None:
216 total = len(self.stats['spew'])
217 seeds = len([i for i in self.stats['spew'] if i['completed'] == 1.0])
218 return seeds, total-seeds
220 def get_pieces_complete(self):
221 """ Returns a list of booleans indicating whether we have completely
222 received that piece of the content. The list of pieces for which
223 we provide this info depends on which files were selected for download
224 using DownloadStartupConfig.set_selected_files().
225 @return A list of booleans
227 if self.stats is None:
230 return self.haveslice
232 def get_vod_prebuffering_progress(self):
233 """ Returns the percentage of prebuffering for Video-On-Demand already
235 @return A float (0..1) """
236 if self.stats is None:
237 if self.status == DLSTATUS_STOPPED and self.progress == 1.0:
242 return self.stats['vod_prebuf_frac']
245 """ Returns if this download is currently in vod mode
248 if self.stats is None:
251 return self.stats['vod']
253 def get_vod_playable(self):
254 """ Returns whether or not the Download started in Video-On-Demand
255 mode has sufficient prebuffer and download speed to be played out
259 if self.stats is None:
262 return self.stats['vod_playable']
264 def get_vod_playable_after(self):
265 """ Returns the estimated time until the Download started in Video-On-Demand
266 mode can be started to play out to the user.
267 @return A number of seconds.
269 if self.stats is None:
270 return float(2 ** 31)
272 return self.stats['vod_playable_after']
274 def get_vod_stats(self):
275 """ Returns a dictionary of collected VOD statistics. The keys contained are:
277 'played' = number of pieces played. With seeking this may be more than npieces
278 'late' = number of pieces arrived after they were due
279 'dropped' = number of pieces lost
280 'stall' = estimation of time the player stalled, waiting for pieces (seconds)
281 'pos' = playback position, as an absolute piece number
282 'prebuf' = amount of prebuffering time that was needed (seconds,
283 set when playback starts)
284 'firstpiece' = starting absolute piece number of selected file
285 'npieces' = number of pieces in selected file
286 </pre>, or no keys if no VOD is in progress.
289 if self.stats is None:
292 return self.stats['vod_stats']
296 def get_log_messages(self):
297 """ Returns the last 10 logged non-fatal error messages.
298 @return A list of (time,msg) tuples. Time is Python time() format. """
299 if self.logmsgs is None:
304 def get_peerlist(self):
305 """ Returns a list of dictionaries, one for each connected peer
306 containing the statistics for that peer. In particular, the
307 dictionary contains the keys:
309 'id' = PeerID or 'http seed'
310 'ip' = IP address as string or URL of httpseed
311 'optimistic' = True/False
312 'direction' = 'L'/'R' (outgoing/incoming)
313 'uprate' = Upload rate in KB/s
314 'uinterested' = Upload Interested: True/False
315 'uchoked' = Upload Choked: True/False
316 'downrate' = Download rate in KB/s
317 'dinterested' = Download interested: True/Flase
318 'dchoked' = Download choked: True/False
319 'snubbed' = Download snubbed: True/False
320 'utotal' = Total uploaded from peer in KB
321 'dtotal' = Total downloaded from peer in KB
322 'completed' = Fraction of download completed by peer (0-1.0)
323 'speed' = The peer's current total download speed (estimated)
326 if self.stats is None or 'spew' not in self.stats:
329 return self.stats['spew']
332 def get_coopdl_helpers(self):
333 """ Returns the peers currently helping.
334 @return A list of PermIDs.
336 if self.coopdl_helpers is None:
339 return self.coopdl_helpers
341 def get_coopdl_coordinator(self):
342 """ Returns the permid of the coordinator when helping that peer
343 in a cooperative download
346 return self.coopdl_coordinator
349 # RePEX: get swarmcache
351 def get_swarmcache(self):
353 Gets the SwarmCache of the Download. If the Download was RePEXing,
354 the latest SwarmCache is returned. If the Download was running
355 normally, a sample of the peerlist is merged with the last
356 known SwarmCache. If the Download was stopped, the last known
357 SwarmCache is returned.
359 @return The latest SwarmCache for this Download, which is a dict
360 mapping dns to a dict with at least 'last_seen' and 'pex' keys.
363 if self.status == DLSTATUS_REPEXING and self.swarmcache is not None:
364 # the swarmcache given at construction comes from RePEXer
365 swarmcache = self.swarmcache
366 elif self.status in [DLSTATUS_DOWNLOADING, DLSTATUS_SEEDING]:
367 # get local PEX peers from peerlist and fill swarmcache
368 peerlist = [p for p in self.get_peerlist() if p['direction']=='L' and p.get('pex_received',0)][:REPEX_SWARMCACHE_SIZE]
370 for peer in peerlist:
371 dns = (peer['ip'], peer['port'])
372 swarmcache[dns] = {'last_seen':self.time,'pex':[]}
373 # fill remainder with peers from old swarmcache
374 if self.swarmcache is not None:
375 for dns in self.swarmcache.keys()[:REPEX_SWARMCACHE_SIZE-len(swarmcache)]:
376 swarmcache[dns] = self.swarmcache[dns]
378 # TODO: move peerlist sampling to a different module?
379 # TODO: perform swarmcache computation only once?
380 elif self.swarmcache is not None:
381 # In all other cases, use the old swarmcache
382 swarmcache = self.swarmcache
383 # TODO: rearrange if statement to merge 1st and 3rd case?