home *** CD-ROM | disk | FTP | other *** search
/ Clickx 47 / Clickx 47.iso / assets / software / Miro_Installer.exe / xulrunner / python / dl_daemon / download.py < prev   
Encoding:
Python Source  |  2008-01-10  |  33.6 KB  |  998 lines

  1. import os
  2. import sys
  3. import bsddb
  4. import shutil
  5. import types
  6. from gettext import gettext as _
  7. from os import remove
  8. import re
  9. from threading import RLock, Event, Thread
  10. import traceback
  11. from copy import copy
  12.  
  13. import libtorrent as lt
  14. from clock import clock
  15. from download_utils import cleanFilename, nextFreeFilename, checkFilenameExtension, filterDirectoryName
  16. from download_utils import filenameFromURL, getFileURLPath
  17. import eventloop
  18. import httpclient
  19. import datetime
  20. import logging
  21. import fileutil
  22.  
  23. import config
  24. import prefs
  25. from sha import sha
  26.  
  27. from dl_daemon import command, daemon
  28. from datetime import timedelta
  29. from util import checkF, checkU, stringify
  30.  
  31. import platformutils
  32. import string
  33.  
  34. chatter = True
  35.  
  36. # a hash of download ids to downloaders
  37. _downloads = {}
  38.  
  39. _lock = RLock()
  40.  
  41. def configReceived ():
  42.     torrentSession.startup()
  43.  
  44. def createDownloader(url, contentType, dlid):
  45.     checkU(url)
  46.     checkU(contentType)
  47.     if contentType == u'application/x-bittorrent':
  48.         return BTDownloader(url, dlid)
  49.     else:
  50.         return HTTPDownloader(url, dlid)
  51.  
  52. # Creates a new downloader object. Returns id on success, None on failure
  53. def startNewDownload(url, dlid, contentType, channelName):
  54.     checkU(url)
  55.     checkU(contentType)
  56.     if channelName:
  57.         checkF(channelName)
  58.     dl = createDownloader(url, contentType, dlid)
  59.     dl.channelName = channelName
  60.     _downloads[dlid] = dl
  61.  
  62. def pauseDownload(dlid):
  63.     try:
  64.         download = _downloads[dlid]
  65.     except: # There is no download with this id
  66.         return True
  67.     return download.pause()
  68.  
  69. def startDownload(dlid):
  70.     try:
  71.         download = _downloads[dlid]
  72.     except KeyError:  # There is no download with this id
  73.         err= u"in startDownload(): no downloader with id %s" % dlid
  74.         c = command.DownloaderErrorCommand(daemon.lastDaemon, err)
  75.         c.send()
  76.         return True
  77.     return download.start()
  78.  
  79. def stopDownload(dlid, delete):
  80.     try:
  81.         _lock.acquire()
  82.         try:
  83.             download = _downloads[dlid]
  84.             del _downloads[dlid]
  85.         finally:
  86.             _lock.release()
  87.     except: # There is no download with this id
  88.         return True
  89.     return download.stop(delete)
  90.  
  91. def stopUpload(dlid):
  92.     try:
  93.         _lock.acquire()
  94.         try:
  95.             download = _downloads[dlid]
  96.             if download.state != u"uploading":
  97.                 return
  98.             del _downloads[dlid]
  99.         finally:
  100.             _lock.release()
  101.     except: # There is no download with this id
  102.         return
  103.     return download.stopUpload()
  104.  
  105. def migrateDownload(dlid, directory):
  106.     checkF(directory)
  107.     try:
  108.         download = _downloads[dlid]
  109.     except: # There is no download with this id
  110.         pass
  111.     else:
  112.         if download.state in (u"finished", u"uploading"):
  113.             download.moveToDirectory(directory)
  114.  
  115. def getDownloadStatus(dlids = None):
  116.     statuses = {}
  117.     for key in _downloads.keys():
  118.         if ((dlids is None)  or (dlids == key) or (key in dlids)):
  119.             try:
  120.                 statuses[key] = _downloads[key].getStatus()
  121.             except:
  122.                 pass
  123.     return statuses
  124.  
  125. def shutDown():
  126.     logging.info ("Shutting down downloaders...")
  127.     for dlid in _downloads:
  128.         _downloads[dlid].shutdown()
  129.     torrentSession.shutdown()
  130.  
  131. def restoreDownloader(downloader):
  132.     downloader = copy(downloader)
  133.     dlerType = downloader.get('dlerType')
  134.     if dlerType == u'HTTP':
  135.         dl = HTTPDownloader(restore = downloader)
  136.     elif dlerType == u'BitTorrent':
  137.         dl = BTDownloader(restore = downloader)
  138.     else:
  139.         err = u"in restoreDownloader(): unknown dlerType: %s" % dlerType
  140.         c = command.DownloaderErrorCommand(daemon.lastDaemon, err)
  141.         c.send()
  142.         return
  143.  
  144.     _downloads[downloader['dlid']] = dl
  145.  
  146. class TorrentSession:
  147.     """Contains the bittorrent session and handles updating all running bittorrents"""
  148.  
  149.     def __init__(self):
  150.         self.torrents = set()
  151.         self.session = None
  152.         self.pnp_on = None
  153.         self.pe_set = None
  154.         self.enc_req = None
  155.  
  156.     def startup(self):
  157.         fingerprint = lt.fingerprint("MR", 1, 1, 0, 0)
  158.         self.session = lt.session(fingerprint)
  159.         self.listen()
  160.         self.setUpnp()
  161.         self.setUploadLimit()
  162.         self.setDownloadLimit()
  163.         self.setEncryption()
  164.         config.addChangeCallback(self.configChanged)
  165.  
  166.     def listen(self):
  167.         self.session.listen_on(config.get(prefs.BT_MIN_PORT), config.get(prefs.BT_MAX_PORT))
  168.  
  169.     def setUpnp(self):
  170.         useUpnp = config.get(prefs.USE_UPNP)
  171.         if useUpnp != self.pnp_on:
  172.             self.pnp_on = useUpnp
  173.             if useUpnp:
  174.                 self.session.start_upnp()
  175.             else:
  176.                 self.session.stop_upnp()
  177.  
  178.     def setUploadLimit(self):
  179.         limit = -1
  180.         if config.get(prefs.LIMIT_UPSTREAM):
  181.             limit = config.get(prefs.UPSTREAM_LIMIT_IN_KBS) * (2 ** 10)
  182.         self.session.set_upload_rate_limit(limit)
  183.  
  184.     def setDownloadLimit(self):
  185.         limit = -1
  186.         if config.get(prefs.LIMIT_DOWNSTREAM_BT):
  187.             limit = config.get(prefs.DOWNSTREAM_BT_LIMIT_IN_KBS) * (2 ** 10)
  188.         self.session.set_download_rate_limit(limit)
  189.  
  190.     def setEncryption(self):
  191.         if self.pe_set is None:
  192.             self.pe_set = lt.pe_settings()
  193.         enc_req = config.get(prefs.BT_ENC_REQ)
  194.         if enc_req != self.enc_req:
  195.             self.enc_req = enc_req
  196.             if enc_req:
  197.                 self.pe_set.in_enc_policy = lt.enc_policy.forced
  198.                 self.pe_set.out_enc_policy = lt.enc_policy.forced
  199.             else:
  200.                 self.pe_set.in_enc_policy = lt.enc_policy.enabled
  201.                 self.pe_set.out_enc_policy = lt.enc_policy.enabled
  202.             self.session.set_pe_settings(self.pe_set)
  203.  
  204.     def shutdown(self):
  205.         config.removeChangeCallback(self.configChanged)
  206.         del self.session
  207.  
  208.     def configChanged(self, key, value):
  209.         if key == prefs.BT_MIN_PORT.key:
  210.             if value > self.session.listen_port():
  211.                 self.listen()
  212.         if key == prefs.BT_MAX_PORT.key:
  213.             if value < self.session.listen_port():
  214.                 self.listen()
  215.         if key == prefs.USE_UPNP.key:
  216.             self.setUpnp()
  217.         if key in (prefs.LIMIT_UPSTREAM.key, prefs.UPSTREAM_LIMIT_IN_KBS.key):
  218.             self.setUploadLimit()
  219.         if key in (prefs.LIMIT_DOWNSTREAM_BT.key, prefs.DOWNSTREAM_BT_LIMIT_IN_KBS.key):
  220.             self.setDownloadLimit()
  221.         if key == prefs.BT_ENC_REQ.key:
  222.             self.setEncryption()
  223.  
  224.     def addTorrent(self, torrent):
  225.         self.torrents.add(torrent)
  226.  
  227.     def removeTorrent(self, torrent):
  228.         try:
  229.             self.torrents.remove(torrent)
  230.         except:
  231.             pass
  232.  
  233.     def updateTorrents(self):
  234.         # Copy this set into a list in case any of the torrents gets removed during the iteration.
  235.         for torrent in [x for x in self.torrents]:
  236.             torrent.updateStatus()
  237.         
  238.  
  239. torrentSession = TorrentSession()
  240.  
  241. class DownloadStatusUpdater:
  242.     """Handles updating status for all in progress downloaders.
  243.  
  244.     On OS X and gtk if the user is on the downloads page and has a bunch of
  245.     downloads going, this can be a fairly CPU intensive task.
  246.     DownloadStatusUpdaters mitigate this in 2 ways.
  247.  
  248.     1) DownloadStatusUpdater objects batch all status updates into one big
  249.     update which takes much less CPU.  
  250.     
  251.     2) The update don't happen fairly infrequently (currently every 5 seconds).
  252.     
  253.     Becouse updates happen infrequently, DownloadStatusUpdaters should only be
  254.     used for progress updates, not events like downloads starting/finishing.
  255.     For those just call updateClient() since they are more urgent, and don't
  256.     happen often enough to cause CPU problems.
  257.     """
  258.  
  259.     UPDATE_CLIENT_INTERVAL = 5
  260.  
  261.     def __init__(self):
  262.         self.toUpdate = set()
  263.  
  264.     def startUpdates(self):
  265.         eventloop.addTimeout(self.UPDATE_CLIENT_INTERVAL, self.doUpdate,
  266.                 "Download status update")
  267.  
  268.     def doUpdate(self):
  269.         try:
  270.             torrentSession.updateTorrents()
  271.             statuses = []
  272.             for downloader in self.toUpdate:
  273.                 statuses.append(downloader.getStatus())
  274.             self.toUpdate = set()
  275.             if statuses:
  276.                 command.BatchUpdateDownloadStatus(daemon.lastDaemon, 
  277.                         statuses).send()
  278.         finally:
  279.             eventloop.addTimeout(self.UPDATE_CLIENT_INTERVAL, self.doUpdate,
  280.                     "Download status update")
  281.  
  282.     def queueUpdate(self, downloader):
  283.         self.toUpdate.add(downloader)
  284.  
  285. downloadUpdater = DownloadStatusUpdater()
  286.  
  287. RETRY_TIMES = (
  288.     60,
  289.     5 * 60,
  290.     10 * 60,
  291.     30 * 60,
  292.     60 * 60,
  293.     2 * 60 * 60,
  294.     6 * 60 * 60,
  295.     24 * 60 * 60
  296.     )
  297.  
  298. class BGDownloader:
  299.     def __init__(self, url, dlid):
  300.         self.dlid = dlid
  301.         self.url = url
  302.         self.startTime = clock()
  303.         self.endTime = self.startTime
  304.         self.shortFilename = filenameFromURL(url)
  305.         self.pickInitialFilename()
  306.         self.state = u"downloading"
  307.         self.currentSize = 0
  308.         self.totalSize = -1
  309.         self.blockTimes = []
  310.         self.shortReasonFailed = self.reasonFailed = u"No Error"
  311.         self.retryTime = None
  312.         self.retryCount = -1
  313.  
  314.     def getURL(self):
  315.         return self.url
  316.  
  317.     def getStatus(self):
  318.         return {'dlid': self.dlid,
  319.             'url': self.url,
  320.             'state': self.state,
  321.             'totalSize': self.totalSize,
  322.             'currentSize': self.currentSize,
  323.             'eta': self.getETA(),
  324.             'rate': self.getRate(),
  325.             'uploaded': 0,
  326.             'filename': self.filename,
  327.             'startTime': self.startTime,
  328.             'endTime': self.endTime,
  329.             'shortFilename': self.shortFilename,
  330.             'reasonFailed': self.reasonFailed,
  331.             'shortReasonFailed': self.shortReasonFailed,
  332.             'dlerType': None,
  333.             'retryTime': self.retryTime,
  334.             'retryCount': self.retryCount,
  335.             'channelName': self.channelName}
  336.  
  337.     def updateClient(self):
  338.         x = command.UpdateDownloadStatus(daemon.lastDaemon, self.getStatus())
  339.         return x.send()
  340.         
  341.     ##
  342.     def pickInitialFilename(self):
  343.         """Pick a path to download to based on self.shortFilename.
  344.  
  345.         This method sets self.filename, as well as creates any leading paths
  346.         needed to start downloading there.
  347.         """
  348.  
  349.         downloadDir = os.path.join(config.get(prefs.MOVIES_DIRECTORY),
  350.                 'Incomplete Downloads')
  351.         # Create the download directory if it doesn't already exist.
  352.         try:
  353.             os.makedirs(downloadDir)
  354.         except:
  355.             pass
  356.         cleaned = cleanFilename(self.shortFilename+".part")
  357.         self.filename = nextFreeFilename(os.path.join(downloadDir, cleaned))
  358.  
  359.     def moveToMoviesDirectory(self):
  360.         """Move our downloaded file from the Incomplete Downloads directoy to
  361.         the movies directory.
  362.         """
  363.         if chatter:
  364.             logging.info ("moving to movies directory filename is %s", self.filename)
  365.         self.moveToDirectory(config.get(prefs.MOVIES_DIRECTORY))
  366.  
  367.     def moveToDirectory (self, directory):
  368.         checkF(directory)
  369.         if self.channelName:
  370.             channelName = filterDirectoryName(self.channelName)
  371.             directory = os.path.join (directory, channelName)
  372.             try:
  373.                 os.makedirs(directory)
  374.             except:
  375.                 pass
  376.         newfilename = os.path.join(directory, self.shortFilename)
  377.         if newfilename == self.filename:
  378.             return
  379.         newfilename = nextFreeFilename(newfilename)
  380.         def callback():
  381.             self.filename = newfilename
  382.             self.updateClient()
  383.         fileutil.migrate_file(self.filename, newfilename, callback)
  384.  
  385.     ##
  386.     # Returns a float with the estimated number of seconds left
  387.     def getETA(self):
  388.         if self.totalSize == -1:
  389.             return -1
  390.         rate = self.getRate()
  391.         if rate > 0:
  392.             return (self.totalSize - self.currentSize)/rate
  393.         else:
  394.             return 0
  395.  
  396.     ##
  397.     # Returns a float with the download rate in bytes per second
  398.     def getRate(self):
  399.         now = clock()
  400.         if self.endTime != self.startTime:
  401.             rate = self.currentSize/(self.endTime-self.startTime)
  402.         else:
  403.             haltedSince = now
  404.             for time, size in reversed(self.blockTimes):
  405.                 if size == self.currentSize:
  406.                     haltedSince = time
  407.                 else:
  408.                     break
  409.             if now - haltedSince > self.HALTED_THRESHOLD:
  410.                 rate = 0
  411.             else:
  412.                 try:
  413.                     timespan = now - self.blockTimes[0][0]
  414.                     if timespan != 0:
  415.                         endSize = self.blockTimes[-1][1]
  416.                         startSize = self.blockTimes[0][1]
  417.                         rate = (endSize - startSize) / timespan
  418.                     else:
  419.                         rate = 0
  420.                 except IndexError:
  421.                     rate = 0
  422.         return rate
  423.  
  424.     def retryDownload(self):
  425.         self.retryDC = None
  426.         self.start()
  427.  
  428.     def handleTemporaryError(self, shortReason, reason):
  429.         self.state = u"offline"
  430.         self.reasonFailed = reason
  431.         self.shortReasonFailed = shortReason
  432.         self.retryCount = self.retryCount + 1
  433.         if self.retryCount >= len (RETRY_TIMES):
  434.             self.retryCount = len (RETRY_TIMES) - 1
  435.         self.retryDC = eventloop.addTimeout (RETRY_TIMES[self.retryCount], self.retryDownload, "Logarithmic retry")
  436.         self.retryTime = datetime.datetime.now() + datetime.timedelta(seconds = RETRY_TIMES[self.retryCount])
  437.         self.updateClient()
  438.  
  439.     def handleError(self, shortReason, reason):
  440.         self.state = u"failed"
  441.         self.reasonFailed = reason
  442.         self.shortReasonFailed = shortReason
  443.         self.updateClient()
  444.  
  445.     def handleNetworkError(self, error):
  446.         if isinstance(error, httpclient.NetworkError):
  447.             if isinstance (error, httpclient.MalformedURL) or \
  448.                isinstance (error, httpclient.UnexpectedStatusCode):
  449.                 self.handleError(error.getFriendlyDescription(),
  450.                                  error.getLongDescription())
  451.             else:
  452.                 self.handleTemporaryError(error.getFriendlyDescription(),
  453.                                           error.getLongDescription())
  454.         else:
  455.             logging.info ("WARNING: grabURL errback not called with NetworkError")
  456.             self.handleError(str(error), str(error))
  457.  
  458.     def handleGenericError(self, longDescription):
  459.         self.handleError(_("Error"), longDescription)
  460.  
  461.     ##
  462.     # Checks the download file size to see if we can accept it based on the 
  463.     # user disk space preservation preference
  464.     def acceptDownloadSize(self, size):
  465.         accept = True
  466.         if config.get(prefs.PRESERVE_DISK_SPACE):
  467.             if size < 0:
  468.                 size = 0
  469.             preserved = config.get(prefs.PRESERVE_X_GB_FREE) * 1024 * 1024 * 1024
  470.             available = platformutils.getAvailableBytesForMovies() - preserved
  471.             accept = (size <= available)
  472.         return accept
  473.  
  474.  
  475. class HTTPDownloader(BGDownloader):
  476.     UPDATE_CLIENT_WINDOW = 12
  477.     HALTED_THRESHOLD = 3 # how many secs until we consider a download halted
  478.  
  479.     def __init__(self, url = None,dlid = None,restore = None):
  480.         self.retryDC = None
  481.         self.channelName = None
  482.         if restore is not None:
  483.             if not isinstance(restore.get('totalSize', 0), int):
  484.                 # Sometimes restoring old downloaders caused errors because
  485.                 # their totalSize wasn't an int.  (see #3965)
  486.                 restore = None
  487.         if restore is not None:
  488.             self.__dict__.update(restore)
  489.             self.blockTimes = []
  490.             self.restartOnError = True
  491.         else:
  492.             BGDownloader.__init__(self, url, dlid)
  493.             self.restartOnError = False
  494.         self.client = None
  495.         self.filehandle = None
  496.         if self.state == 'downloading':
  497.             self.startDownload()
  498.         elif self.state == 'offline':
  499.             self.start()
  500.         else:
  501.             self.updateClient()
  502.  
  503.     def resetBlockTimes(self):
  504.         self.blockTimes = [(clock(), self.currentSize)]
  505.  
  506.     def startNewDownload(self):
  507.         self.currentSize = 0
  508.         self.totalSize = -1
  509.         self.startDownload()
  510.  
  511.     def startDownload(self):
  512.         if self.retryDC:
  513.             self.retryDC.cancel()
  514.             self.retryDC = None
  515.         if self.currentSize == 0:
  516.             headerCallback = self.onHeaders
  517.         else:
  518.             headerCallback = self.onHeadersRestart
  519.         self.client = httpclient.grabURL(self.url,
  520.                 self.onDownloadFinished, self.onDownloadError,
  521.                 headerCallback, self.onBodyData, start=self.currentSize)
  522.         self.resetBlockTimes()
  523.         self.updateClient()
  524.  
  525.     def cancelRequest(self):
  526.         if self.client is not None:
  527.             self.client.cancel()
  528.             self.client = None
  529.  
  530.     def handleError(self, shortReason, reason):
  531.         BGDownloader.handleError(self, shortReason, reason)
  532.         self.cancelRequest()
  533.         try:
  534.             remove (self.filename)
  535.         except:
  536.             pass
  537.         self.currentSize = 0
  538.         self.totalSize = -1
  539.  
  540.     def handleTemporaryError(self, shortReason, reason):
  541.         BGDownloader.handleTemporaryError(self, shortReason, reason)
  542.         self.cancelRequest()
  543.  
  544.     def handleWriteError(self, error):
  545.         self.handleGenericError(_("Could not write to %s") % 
  546.                                   stringify(self.filename))
  547.         if self.filehandle is not None:
  548.             try:
  549.                 self.filehandle.close()
  550.             except:
  551.                 pass
  552.         try:
  553.             remove(self.filename)
  554.         except:
  555.             pass
  556.  
  557.     def onHeaders(self, info):
  558.         if info['contentLength'] != None:
  559.             self.totalSize = info['contentLength']
  560.         if self.client.gotBadStatusCode:
  561.             error = httpclient.UnexpectedStatusCode(info['status'])
  562.             self.handleNetworkError(error)
  563.             return
  564.         if not self.acceptDownloadSize(self.totalSize):
  565.             self.handleError(_("Not enough disk space"),
  566.                 _("%s MB required to store this video") % 
  567.                 (self.totalSize / (2 ** 20)))
  568.             return
  569.         #We have a success
  570.         self.retryCount = -1
  571.         #Get the length of the file, then create it
  572.         self.shortFilename = cleanFilename(info['filename'])
  573.         self.shortFilename = checkFilenameExtension(self.shortFilename, info)
  574.         self.pickInitialFilename()
  575.         try:
  576.             self.filehandle = file(self.filename,"w+b")
  577.         except IOError:
  578.             self.handleGenericError("Couldn't open %s for writing" % 
  579.                 stringify(self.filename))
  580.             return
  581.         if self.totalSize > 0:
  582.             try:
  583.                 self.filehandle.seek(self.totalSize-1)
  584.                 self.filehandle.write(' ')
  585.                 self.filehandle.seek(0)
  586.             except IOError, error:
  587.                 self.handleWriteError(error)
  588.                 return
  589.         self.updateClient()
  590.  
  591.     def onHeadersRestart(self, info):
  592.         self.restartOnError = False
  593.         if info['status'] != 206 or 'content-range' not in info:
  594.             self.currentSize = 0
  595.             self.totalSize = -1
  596.             self.resetBlockTimes()
  597.             if not self.client.gotBadStatusCode:
  598.                 self.onHeaders(info)
  599.             else:
  600.                 self.cancelRequest()
  601.                 self.startNewDownload()
  602.             return
  603.         try:
  604.             self.parseContentRange(info['content-range'])
  605.         except ValueError:
  606.             logging.info ("WARNING, bad content-range: %r", info['content-range'])
  607.             logging.info ("currentSize: %d totalSize: %d", self.currentSize,
  608.                           self.totalSize)
  609.             self.cancelRequest()
  610.             self.startNewDownload()
  611.         else:
  612.             try:
  613.                 self.filehandle = file(self.filename,"r+b")
  614.                 self.filehandle.seek(self.currentSize)
  615.             except IOError, e:
  616.                 self.handleWriteError(e)
  617.             #We have a success
  618.             self.retryCount = -1
  619.         self.updateClient()
  620.  
  621.     def parseContentRange(self, contentRange):
  622.         """Parse the content-range header from an http response.  If it's
  623.         badly formatted, or it's not what we were expecting based on the state
  624.         we restored to, raise a ValueError.
  625.         """
  626.  
  627.         m = re.search('bytes\s+(\d+)-(\d+)/(\d+)', contentRange)
  628.         if m is None:
  629.             raise ValueError()
  630.         start = int(m.group(1))
  631.         end = int(m.group(2))
  632.         totalSize = int(m.group(3))
  633.         if start > self.currentSize or (end + 1 != totalSize):
  634.             # we only have the 1st <self.currentSize> bytes of the file, so
  635.             # we cant handle these responses
  636.             raise ValueError()
  637.         self.currentSize = start
  638.         self.totalSize = totalSize
  639.  
  640.     def onDownloadError(self, error):
  641.         if self.restartOnError:
  642.             self.restartOnError = False
  643.             self.startDownload()
  644.         else:
  645.             self.client = None
  646.             self.handleNetworkError(error)
  647.  
  648.     def onBodyData(self, data):
  649.         if self.state != 'downloading':
  650.             return
  651.         self.updateRateAndETA(len(data))
  652.         downloadUpdater.queueUpdate(self)
  653.         try:
  654.             self.filehandle.write(data)
  655.         except IOError, e:
  656.             self.handleWriteError(e)
  657.  
  658.     def onDownloadFinished(self, response):
  659.         self.client = None
  660.         try:
  661.             self.filehandle.close()
  662.         except Exception, e:
  663.             self.handleWriteError(e)
  664.             return
  665.         self.state = "finished"
  666.         if self.totalSize == -1:
  667.             self.totalSize = self.currentSize
  668.         self.endTime = clock()
  669.         try:
  670.             self.moveToMoviesDirectory()
  671.         except IOError, e:
  672.             self.handleWriteError(e)
  673.         self.resetBlockTimes()
  674.         self.updateClient()
  675.  
  676.     def getStatus(self):
  677.         data = BGDownloader.getStatus(self)
  678.         data['dlerType'] = 'HTTP'
  679.         return data
  680.  
  681.     ##
  682.     # Update the download rate and eta based on recieving length bytes
  683.     def updateRateAndETA(self, length):
  684.         now = clock()
  685.         self.currentSize = self.currentSize + length
  686.         self.blockTimes.append((now,  self.currentSize))
  687.         window_start = now - self.UPDATE_CLIENT_WINDOW
  688.         i = 0
  689.         for i in xrange (len(self.blockTimes)):
  690.             if self.blockTimes[0][0] >= window_start:
  691.                 break
  692.         self.blockTimes = self.blockTimes[i:]
  693.  
  694.     ##
  695.     # Pauses the download.
  696.     def pause(self):
  697.         if self.state != "stopped":
  698.             self.cancelRequest()
  699.             self.state = "paused"
  700.             self.updateClient()
  701.  
  702.     ##
  703.     # Stops the download and removes the partially downloaded
  704.     # file.
  705.     def stop(self, delete):
  706.         if self.state == "downloading":
  707.             if self.filehandle is not None:
  708.                 try:
  709.                     if not self.filehandle.closed:
  710.                         self.filehandle.close()
  711.                     remove(self.filename)
  712.                 except:
  713.                     pass
  714.         if delete:
  715.             try:
  716.                 if os.path.isdir(self.filename):
  717.                     shutil.rmtree(self.filename)
  718.                 else:
  719.                     remove(self.filename)
  720.             except:
  721.                 pass
  722.         self.currentSize = 0
  723.         self.cancelRequest()
  724.         self.state = "stopped"
  725.         self.updateClient()
  726.  
  727.     def stopUpload(self):
  728.         # HTTP downloads never upload.
  729.         pass
  730.  
  731.     ##
  732.     # Continues a paused or stopped download thread
  733.     def start(self):
  734.         if self.state in ('paused', 'stopped', 'offline'):
  735.             self.state = "downloading"
  736.             self.startDownload()
  737.  
  738.     def shutdown(self):
  739.         self.cancelRequest()
  740.         self.updateClient()
  741.  
  742. class BTDownloader(BGDownloader):
  743.     def __init__(self, url = None, item = None, restore = None):
  744.         self.metainfo = None
  745.         self.torrent = None
  746.         self.rate = self.eta = 0
  747.         self.upRate = self.uploaded = 0
  748.         self.activity = None
  749.         self.fastResumeData = None
  750.         self.retryDC = None
  751.         self.channelName = None
  752.         self.uploadedStart = 0
  753.         self.restarting = False
  754.         self.seeders = -1
  755.         self.leechers = -1
  756.         if restore is not None:
  757.             self.firstTime = False
  758.             self.restoreState(restore)
  759.         else:
  760.             self.firstTime = True
  761.             BGDownloader.__init__(self,url,item)
  762.             self.runDownloader()
  763.  
  764.     def _startTorrent(self):
  765.         try:
  766.             torrent_info = lt.torrent_info(lt.bdecode(self.metainfo))
  767.             self.totalSize = torrent_info.total_size()
  768.  
  769.             if self.firstTime and not self.acceptDownloadSize(self.totalSize):
  770.                 self.handleError(_("Not enough disk space"),
  771.                                  _("%s MB required to store this video") % 
  772.                                  (self.totalSize / (2 ** 20)))
  773.                 return
  774.  
  775.             name = stringify(self.filename)
  776.             if self.fastResumeData:
  777.                 resume = lt.bdecode(self.fastResumeData)
  778.                 self.torrent = torrentSession.session.add_torrent(torrent_info, name, lt.bdecode(self.fastResumeData), lt.storage_mode_t.storage_mode_allocate)
  779.             else:
  780.                 self.torrent = torrentSession.session.add_torrent(torrent_info, name, None, lt.storage_mode_t.storage_mode_allocate)
  781.         except:
  782.             self.handleError(_('BitTorrent failure'), 
  783.                     _('BitTorrent failed to startup'))
  784.         else:
  785.             torrentSession.addTorrent (self)
  786.  
  787.     def _shutdownTorrent(self):
  788.         try:
  789.             torrentSession.removeTorrent (self)
  790.             if self.torrent is not None:
  791.                 self.torrent.pause()
  792.                 self.fastResumeData = lt.bencode(self.torrent.write_resume_data())
  793.                 torrentSession.session.remove_torrent(self.torrent, 0)
  794.                 self.torrent = None
  795.         except:
  796.             logging.exception ("DTV: Warning: Error shutting down torrent")
  797.  
  798.     def _pauseTorrent(self):
  799.         try:
  800.             torrentSession.removeTorrent (self)
  801.             if self.torrent is not None:
  802.                 self.torrent.pause()
  803.         except:
  804.             logging.exception ("DTV: Warning: Error pausing torrent")
  805.  
  806.     def _resumeTorrent(self):
  807.         if self.torrent is not None:
  808.             try:
  809.                 self.torrent.resume()
  810.                 torrentSession.addTorrent (self)
  811.             except:
  812.                 logging.exception ("DTV: Warning: Error resuming torrent")
  813.         else:
  814.             self._startTorrent()
  815.  
  816.     def updateStatus(self):
  817.         """
  818.         activity -- string specifying what's currently happening or None for
  819.                 normal operations.  
  820.         upRate -- upload rate in B/s
  821.         downRate -- download rate in B/s
  822.         upTotal -- total MB uploaded
  823.         downTotal -- total MB downloaded
  824.         fractionDone -- what portion of the download is completed.
  825.         timeEst -- estimated completion time, in seconds.
  826.         totalSize -- total size of the torrent in bytes
  827.         """
  828.  
  829.         status = self.torrent.status()
  830.         self.totalSize = status.total_wanted
  831.         self.rate = status.download_payload_rate
  832.         self.upRate = status.upload_payload_rate
  833.         self.uploaded = status.total_payload_upload + self.uploadedStart
  834.         self.seeders = status.num_complete
  835.         self.leechers = status.num_incomplete
  836.         try:
  837.             self.eta = (status.total_wanted - status.total_wanted_done) / float (status.download_payload_rate)
  838.         except ZeroDivisionError:
  839.             self.eta = 0
  840.         if status.state == lt.torrent_status.states.queued_for_checking:
  841.             self.activity = "waiting to check existing files"
  842.         elif status.state == lt.torrent_status.states.checking_files:
  843.             self.activity = "checking existing files"
  844.         elif status.state == lt.torrent_status.states.connecting_to_tracker:
  845.             self.activity = "connecting to peers"
  846.         elif status.state == lt.torrent_status.states.allocating:
  847.             self.activity = "allocating disk space"
  848.         else:
  849.             self.activity = None
  850.         self.currentSize = status.total_wanted_done
  851.         if self.state == "downloading" and status.state == lt.torrent_status.states.seeding:
  852.             self.moveToMoviesDirectory()
  853.             self.state = "uploading"
  854.             self.endTime = clock()
  855.             self.updateClient()
  856.         else:
  857.             downloadUpdater.queueUpdate(self)
  858.  
  859.     def handleError(self, shortReason, reason):
  860.         self._shutdownTorrent()
  861.         BGDownloader.handleError(self, shortReason, reason)
  862.  
  863.     def handleTemporaryError(self, shortReason, reason):
  864.         self._shutdownTorrent()
  865.         BGDownloader.handleTemporaryError(self, shortReason, reason)
  866.  
  867.     def moveToDirectory(self, directory):
  868.         if self.state in ('uploading', 'downloading'):
  869.             self._shutdownTorrent()
  870.             BGDownloader.moveToDirectory(self, directory)
  871.             self._resumeTorrent()
  872.         else:
  873.             BGDownloader.moveToDirectory(self, directory)
  874.  
  875.     def restoreState(self, data):
  876.         self.__dict__.update(data)
  877.         self.rate = self.eta = 0
  878.         self.upRate = 0
  879.         self.uploadedStart = self.uploaded
  880.         if self.state in ('downloading', 'uploading'):
  881.             self.runDownloader(done=True)
  882.         elif self.state == 'offline':
  883.             self.start()
  884.  
  885.     def getStatus(self):
  886.         data = BGDownloader.getStatus(self)
  887.         data['upRate'] = self.upRate
  888.         data['uploaded'] = self.uploaded
  889.         data['metainfo'] = self.metainfo
  890.         data['fastResumeData'] = self.fastResumeData
  891.         data['activity'] = self.activity
  892.         data['dlerType'] = 'BitTorrent'
  893.         data['seeders'] = self.seeders
  894.         data['leechers'] = self.leechers
  895.         return data
  896.  
  897.     def getRate(self):
  898.         return self.rate
  899.  
  900.     def getETA(self):
  901.         return self.eta
  902.         
  903.     def pause(self):
  904.         self.state = "paused"
  905.         self._pauseTorrent()
  906.         self.updateClient()
  907.  
  908.     def stop(self, delete):
  909.         self.state = "stopped"
  910.         self._shutdownTorrent()
  911.         self.updateClient()
  912.         if delete:
  913.             try:
  914.                 if os.path.isdir(self.filename):
  915.                     shutil.rmtree(self.filename)
  916.                 else:
  917.                     remove(self.filename)
  918.             except:
  919.                 pass
  920.  
  921.     def stopUpload(self):
  922.         self.state = "finished"
  923.         self._shutdownTorrent()
  924.         self.updateClient()
  925.  
  926.     def start(self):
  927.         if self.state not in ('paused', 'stopped', 'offline'):
  928.             return
  929.  
  930.         self.state = "downloading"
  931.         if self.retryDC:
  932.             self.retryDC.cancel()
  933.             self.retryDC = None
  934.         self.updateClient()
  935.         self.getMetainfo()
  936.  
  937.     def shutdown(self):
  938.         self._shutdownTorrent()
  939.         self.updateClient()
  940.  
  941.     def gotMetainfo(self):
  942.         # FIXME: If the client is stopped before a BT download gets
  943.         #        its metadata, we never run this. It's not a huge deal
  944.         #        because it only affects the incomplete filename
  945.         if not self.restarting:
  946.             try:
  947.                 metainfo = lt.bdecode(self.metainfo)
  948.                 name = metainfo['info']['name']
  949.             except (RuntimeError):
  950.                 self.handleError(_("Corrupt Torrent"),
  951.                         _("The torrent file at %s was not valid") % stringify(self.url))
  952.                 return
  953.             name = name.decode('utf-8', 'replace')
  954.             self.shortFilename = cleanFilename(name)
  955.             self.pickInitialFilename()
  956.         self.updateClient()
  957.         self._resumeTorrent()
  958.  
  959.     def handleMetainfo(self, metainfo):
  960.         self.metainfo = metainfo
  961.         self.gotMetainfo()
  962.  
  963.     def onDescriptionDownload(self, info):
  964.         self.handleMetainfo (info['body'])
  965.  
  966.     def onDescriptionDownloadFailed(self, exception):
  967.         self.handleNetworkError(exception)
  968.  
  969.     def getMetainfo(self):
  970.         if self.metainfo is None:
  971.             if self.url.startswith('file://'):
  972.                 path = getFileURLPath(self.url)
  973.                 try:
  974.                     metainfoFile = open(path, 'rb')
  975.                 except IOError:
  976.                     self.handleError(_("Torrent file deleted"),
  977.                             _("The torrent file for this item was deleted "
  978.                                 "outside of Miro."))
  979.  
  980.                     return
  981.                 try:
  982.                     metainfo = metainfoFile.read()
  983.                 finally:
  984.                     metainfoFile.close()
  985.  
  986.                 self.handleMetainfo(metainfo)
  987.             else:
  988.                 httpclient.grabURL(self.getURL(), self.onDescriptionDownload,
  989.                         self.onDescriptionDownloadFailed)
  990.         else:
  991.             self.gotMetainfo()
  992.                 
  993.  
  994.     def runDownloader(self,done=False):
  995.         self.restarting = done
  996.         self.updateClient()
  997.         self.getMetainfo()
  998.