1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
32
from threading import RLock
38
from miro.gtcache import gettext as _
40
import libtorrent as lt
41
from miro.clock import clock
42
from miro.download_utils import (
43
clean_filename, next_free_filename, check_filename_extension,
44
filter_directory_name, filename_from_url, get_file_url_path)
45
from miro import eventloop
46
from miro import httpclient
47
from miro import fileutil
49
from miro import config
50
from miro import prefs
52
from miro.dl_daemon import command, daemon
53
from miro.util import check_f, check_u, stringify, MAX_TORRENT_SIZE
54
from miro.plat.utils import get_available_bytes_for_movies, utf8_to_filename
58
# a hash of download ids to downloaders
63
def config_received():
64
TORRENT_SESSION.startup()
66
def create_downloader(url, contentType, dlid):
69
if contentType == u'application/x-bittorrent':
70
return BTDownloader(url, dlid)
72
return HTTPDownloader(url, dlid, expectedContentType=contentType)
74
def start_new_download(url, dlid, contentType, channelName):
75
"""Creates a new downloader object.
77
Returns id on success, None on failure.
83
dl = create_downloader(url, contentType, dlid)
84
dl.channelName = channelName
87
def pause_download(dlid):
89
download = _downloads[dlid]
91
# There is no download with this id
93
return download.pause()
95
def info_hash_to_long(info_hash):
96
"""The info_hash() method from libtorrent returns a "big_number" object.
97
This doesn't hash very well: different instances with the same value
98
will have different hashes. So we need to convert them to long objects,
99
though this weird process.
101
return long(str(info_hash), 16)
103
def start_download(dlid):
105
download = _downloads[dlid]
107
# There is no download with this id
108
err = u"in start_download(): no downloader with id %s" % dlid
109
c = command.DownloaderErrorCommand(daemon.LAST_DAEMON, err)
112
return download.start()
114
def stop_download(dlid, delete):
117
download = _downloads[dlid]
120
# There is no download with this id
125
return download.stop(delete)
127
def stop_upload(dlid):
130
download = _downloads[dlid]
131
if download.state not in (u"uploading", u"uploading-paused"):
135
# There is no download with this id
139
return download.stop_upload()
141
def pause_upload(dlid):
144
download = _downloads[dlid]
145
if download.state != u"uploading":
149
# There is no download with this id
153
return download.pause_upload()
155
def migrate_download(dlid, directory):
158
download = _downloads[dlid]
160
# There is no download with this id
163
if download.state in (u"finished", u"uploading", u"uploading-paused"):
164
download.move_to_directory(directory)
166
def get_download_status(dlids=None):
168
for key in _downloads.keys():
169
if dlids is None or dlids == key or key in dlids:
171
statuses[key] = _downloads[key].get_status()
177
logging.info("Shutting down downloaders...")
178
for dlid in _downloads:
179
_downloads[dlid].shutdown()
180
logging.info("Shutting down torrent session...")
181
TORRENT_SESSION.shutdown()
182
logging.info("shutdown() finished")
184
def restore_downloader(downloader):
185
if downloader['dlid'] in _downloads:
186
logging.warn("Not restarting active downloader: %s",
190
downloader = copy(downloader)
191
dler_type = downloader.get('dlerType')
192
if dler_type == u'HTTP':
193
dl = HTTPDownloader(restore=downloader)
194
elif dler_type == u'BitTorrent':
195
dl = BTDownloader(restore=downloader)
197
err = u"in restore_downloader(): unknown dlerType: %s" % dler_type
198
c = command.DownloaderErrorCommand(daemon.LAST_DAEMON, err)
202
_downloads[downloader['dlid']] = dl
204
class TorrentSession(object):
205
"""Contains the bittorrent session and handles updating all
209
self.torrents = set()
210
self.info_hash_to_downloader = {}
217
fingerprint = lt.fingerprint("MR", 1, 1, 0, 0)
218
self.session = lt.session(fingerprint)
221
self.set_upload_limit()
222
self.set_download_limit()
223
self.set_encryption()
224
config.add_change_callback(self.config_changed)
227
self.session.listen_on(config.get(prefs.BT_MIN_PORT),
228
config.get(prefs.BT_MAX_PORT))
231
use_upnp = config.get(prefs.USE_UPNP)
232
if use_upnp == self.pnp_on:
234
self.pnp_on = use_upnp
236
self.session.start_upnp()
238
self.session.stop_upnp()
240
def set_upload_limit(self):
242
if config.get(prefs.LIMIT_UPSTREAM):
243
limit = config.get(prefs.UPSTREAM_LIMIT_IN_KBS)
244
limit = limit * (2 ** 10)
245
if limit > sys.maxint:
246
# avoid OverflowErrors by keeping the value an integer
248
self.session.set_upload_rate_limit(limit)
250
def set_download_limit(self):
252
if config.get(prefs.LIMIT_DOWNSTREAM_BT):
253
limit = config.get(prefs.DOWNSTREAM_BT_LIMIT_IN_KBS)
254
limit = limit * (2 ** 10)
255
if limit > sys.maxint:
256
# avoid OverflowErrors by keeping the value an integer
258
self.session.set_download_rate_limit(limit)
260
def set_connection_limit(self):
262
if config.get(prefs.LIMIT_CONNECTIONS_BT):
263
limit = config.get(prefs.CONNECTION_LIMIT_BT_NUM)
265
# there are only 2**16 TCP port numbers
267
self.session.set_max_connections(limit)
269
def set_encryption(self):
270
if self.pe_set is None:
271
self.pe_set = lt.pe_settings()
272
enc_req = config.get(prefs.BT_ENC_REQ)
273
if enc_req != self.enc_req:
274
self.enc_req = enc_req
276
self.pe_set.in_enc_policy = lt.enc_policy.forced
277
self.pe_set.out_enc_policy = lt.enc_policy.forced
279
self.pe_set.in_enc_policy = lt.enc_policy.enabled
280
self.pe_set.out_enc_policy = lt.enc_policy.enabled
281
self.session.set_pe_settings(self.pe_set)
284
self.session.stop_upnp()
285
config.remove_change_callback(self.config_changed)
287
def config_changed(self, key, value):
288
if key == prefs.BT_MIN_PORT.key:
289
if value > self.session.listen_port():
291
elif key == prefs.BT_MAX_PORT.key:
292
if value < self.session.listen_port():
294
elif key == prefs.USE_UPNP.key:
296
elif key in (prefs.LIMIT_UPSTREAM.key,
297
prefs.UPSTREAM_LIMIT_IN_KBS.key):
298
self.set_upload_limit()
299
elif key in (prefs.LIMIT_DOWNSTREAM_BT.key,
300
prefs.DOWNSTREAM_BT_LIMIT_IN_KBS.key):
301
self.set_download_limit()
302
elif key == prefs.BT_ENC_REQ.key:
303
self.set_encryption()
304
elif key in (prefs.LIMIT_CONNECTIONS_BT.key,
305
prefs.CONNECTION_LIMIT_BT_NUM.key):
306
self.set_connection_limit()
308
def find_duplicate_torrent(self, torrent_info):
309
info_hash = info_hash_to_long(torrent_info.info_hash())
310
return self.info_hash_to_downloader.get(info_hash)
312
def add_torrent(self, downloader):
313
self.torrents.add(downloader)
314
info_hash = info_hash_to_long(downloader.torrent.info_hash())
315
self.info_hash_to_downloader[info_hash] = downloader
317
def remove_torrent(self, downloader):
318
if downloader in self.torrents:
319
self.torrents.remove(downloader)
320
info_hash = info_hash_to_long(downloader.torrent.info_hash())
321
del self.info_hash_to_downloader[info_hash]
323
def update_torrents(self):
324
# Copy this set into a list in case any of the torrents gets
325
# removed during the iteration.
326
for torrent in [x for x in self.torrents]:
327
torrent.update_status()
329
TORRENT_SESSION = TorrentSession()
331
class DownloadStatusUpdater(object):
332
"""Handles updating status for all in progress downloaders.
334
On OS X and gtk if the user is on the downloads page and has a
335
bunch of downloads going, this can be a fairly CPU intensive task.
336
DownloadStatusUpdaters mitigate this in 2 ways.
338
1. DownloadStatusUpdater objects batch all status updates into one
339
big update which takes much less CPU.
341
2. The update don't happen fairly infrequently (currently every 5
344
Because updates happen infrequently, DownloadStatusUpdaters should
345
only be used for progress updates, not events like downloads
346
starting/finishing. For those just call update_client() since they
347
are more urgent, and don't happen often enough to cause CPU
351
UPDATE_CLIENT_INTERVAL = 1
354
self.to_update = set()
356
def start_updates(self):
357
eventloop.add_timeout(self.UPDATE_CLIENT_INTERVAL, self.do_update,
358
"Download status update")
362
TORRENT_SESSION.update_torrents()
364
for downloader in self.to_update:
365
statuses.append(downloader.get_status())
366
self.to_update = set()
368
command.BatchUpdateDownloadStatus(daemon.LAST_DAEMON,
371
eventloop.add_timeout(self.UPDATE_CLIENT_INTERVAL, self.do_update,
372
"Download status update")
374
def queue_update(self, downloader):
375
self.to_update.add(downloader)
377
DOWNLOAD_UPDATER = DownloadStatusUpdater()
390
class BGDownloader(object):
391
def __init__(self, url, dlid):
394
self.startTime = clock()
395
self.endTime = self.startTime
396
self.shortFilename = filename_from_url(url)
397
self.pick_initial_filename()
398
self.state = u"downloading"
401
self.shortReasonFailed = self.reasonFailed = u"No Error"
402
self.retryTime = None
408
def get_status(self):
409
return {'dlid': self.dlid,
412
'totalSize': self.totalSize,
413
'currentSize': self.currentSize,
414
'eta': self.get_eta(),
415
'rate': self.get_rate(),
417
'filename': self.filename,
418
'startTime': self.startTime,
419
'endTime': self.endTime,
420
'shortFilename': self.shortFilename,
421
'reasonFailed': self.reasonFailed,
422
'shortReasonFailed': self.shortReasonFailed,
424
'retryTime': self.retryTime,
425
'retryCount': self.retryCount,
426
'channelName': self.channelName}
428
def update_client(self):
429
x = command.UpdateDownloadStatus(daemon.LAST_DAEMON, self.get_status())
432
def pick_initial_filename(self, suffix=".part", torrent=False):
433
"""Pick a path to download to based on self.shortFilename.
435
This method sets self.filename, as well as creates any leading
436
paths needed to start downloading there.
438
If the torrent flag is true, then the filename we're working
439
with is utf-8 and shouldn't be transformed in any way.
441
If the torrent flag is false, then the filename we're working
442
with is ascii and needs to be transformed into something sane.
445
download_dir = os.path.join(config.get(prefs.MOVIES_DIRECTORY),
446
'Incomplete Downloads')
447
# Create the download directory if it doesn't already exist.
448
if not os.path.exists(download_dir):
449
fileutil.makedirs(download_dir)
450
filename = self.shortFilename + suffix
452
# this is an ascii filename and needs to be fixed
453
filename = clean_filename(filename)
454
self.filename = next_free_filename(
455
os.path.join(download_dir, filename))
457
def move_to_movies_directory(self):
458
"""Move our downloaded file from the Incomplete Downloads
459
directory to the movies directory.
462
logging.info("move_to_movies_directory: filename is %s",
464
self.move_to_directory(config.get(prefs.MOVIES_DIRECTORY))
466
def move_to_directory(self, directory):
469
channel_name = filter_directory_name(self.channelName)
470
# bug 10769: shutil and windows has problems with long
471
# filenames, so we clip the directory name.
472
if len(channel_name) > 80:
473
channel_name = channel_name[:80]
474
directory = os.path.join(directory, channel_name)
475
if not os.path.exists(directory):
477
fileutil.makedirs(directory)
478
except (SystemExit, KeyboardInterrupt):
482
newfilename = os.path.join(directory, self.shortFilename)
483
if newfilename == self.filename:
485
newfilename = next_free_filename(newfilename)
487
self.filename = newfilename
489
fileutil.migrate_file(self.filename, newfilename, callback)
492
"""Returns a float with the estimated number of seconds left.
494
if self.totalSize == -1:
496
rate = self.get_rate()
498
return (self.totalSize - self.currentSize) / rate
503
"""Returns a float with the download rate in bytes per second
505
if self.endTime != self.startTime:
506
rate = self.currentSize / (self.endTime - self.startTime)
511
def retry_download(self):
513
self.start(resume=False)
515
def handle_temporary_error(self, shortReason, reason):
516
self.state = u"offline"
517
self.endTime = self.startTime = 0
519
self.reasonFailed = reason
520
self.shortReasonFailed = shortReason
521
self.retryCount = self.retryCount + 1
522
if self.retryCount >= len(RETRY_TIMES):
523
self.retryCount = len(RETRY_TIMES) - 1
524
self.retryDC = eventloop.add_timeout(
525
RETRY_TIMES[self.retryCount], self.retry_download,
527
now = datetime.datetime.now()
528
self.retryTime = now + datetime.timedelta(seconds=RETRY_TIMES[self.retryCount])
529
logging.info("Temporary error: '%s' '%s'. retrying at %s %s",
530
shortReason, reason, self.retryTime, self.retryCount)
533
def handle_error(self, shortReason, reason):
534
self.state = u"failed"
535
self.reasonFailed = reason
536
self.shortReasonFailed = shortReason
539
def handle_network_error(self, error):
540
if isinstance(error, httpclient.NetworkError):
541
if (isinstance(error, httpclient.MalformedURL)
542
or isinstance(error, httpclient.UnknownHostError)
543
or isinstance(error, httpclient.AuthorizationFailed)
544
or isinstance(error, httpclient.ProxyAuthorizationFailed)
545
or isinstance(error, httpclient.UnexpectedStatusCode)):
546
self.handle_error(error.getFriendlyDescription(),
547
error.getLongDescription())
548
self.retryCount = -1 # reset retryCount
550
self.handle_temporary_error(error.getFriendlyDescription(),
551
error.getLongDescription())
553
logging.info("WARNING: grab_url errback not called with "
555
self.handle_error(str(error), str(error))
557
def handle_generic_error(self, longDescription):
558
self.handle_error(_("Error"), longDescription)
560
def accept_download_size(self, size):
561
"""Checks the download file size to see if we can accept it
562
based on the user disk space preservation preference
565
if config.get(prefs.PRESERVE_DISK_SPACE):
568
preserved = (config.get(prefs.PRESERVE_X_GB_FREE) *
570
available = get_available_bytes_for_movies() - preserved
571
accept = (size <= available)
574
class HTTPDownloader(BGDownloader):
575
CHECK_STATS_TIMEOUT = 1.0
577
def __init__(self, url=None, dlid=None, restore=None,
578
expectedContentType=None):
580
self.channelName = None
581
self.expectedContentType = expectedContentType
582
if restore is not None:
583
if not isinstance(restore.get('totalSize', 0), int):
584
# Sometimes restoring old downloaders caused errors
585
# because their totalSize wasn't an int. (see #3965)
586
restore['totalSize'] = int(restore['totalSize'])
587
self.__dict__.update(restore)
588
self.restartOnError = True
590
BGDownloader.__init__(self, url, dlid)
591
self.restartOnError = False
594
if self.state == 'downloading':
595
self.start_download()
596
elif self.state == 'offline':
601
def start_new_download(self):
602
"""Start a download, discarding any existing data"""
605
self.start_download(resume=False)
607
def start_download(self, resume=True):
609
self.retryDC.cancel()
612
resume = self._resume_sanity_check()
614
logging.info("start_download: %s", self.url)
616
self.client = httpclient.grab_url(
617
self.url, self.on_download_finished, self.on_download_error,
618
header_callback=self.on_headers, write_file=self.filename,
621
eventloop.add_timeout(self.CHECK_STATS_TIMEOUT, self.update_stats,
622
'update http downloader stats')
624
def _resume_sanity_check(self):
625
"""Do sanity checks to test if we should try HTTP Resume.
627
:returns: If we should still try HTTP resume
629
if not os.path.exists(self.filename):
631
# sanity check that the file we're resuming from is the right
632
# size. In particular, before the libcurl change, we would
633
# preallocate the entire file, so we need to undo this.
634
file_size = os.stat(self.filename)[stat.ST_SIZE]
635
if file_size > self.currentSize:
636
# use logging.info rather than warn, since this is the
637
# usual case from upgrading from 3.0.x to 3.1
638
logging.info("File larger than currentSize: truncating. "
639
"url: %s, path: %s.", self.url, self.filename)
640
f = open(self.filename, "ab")
641
f.truncate(self.currentSize)
643
elif file_size < self.currentSize:
644
# Data got deleted somehow. Let's start over.
645
logging.warn("File doesn't contain enough data to resume. "
646
"url: %s, path: %s.", self.url, self.filename)
650
def destroy_client(self):
651
"""update the stats before we throw away the client.
656
def cancel_request(self, remove_file=False):
657
if self.client is not None:
658
self.client.cancel(remove_file=remove_file)
659
self.destroy_client()
660
# if it's in a retrying state, we want to nix that, too
662
self.retryDC.cancel()
665
def handle_error(self, shortReason, reason):
666
BGDownloader.handle_error(self, shortReason, reason)
667
self.cancel_request()
668
if os.path.exists(self.filename):
670
fileutil.remove(self.filename)
676
def handle_temporary_error(self, shortReason, reason):
677
self.cancel_request()
678
BGDownloader.handle_temporary_error(self, shortReason, reason)
680
def handle_write_error(self, error):
681
text = (_("Could not write to %(filename)s") %
682
{"filename": stringify(self.filename)})
683
self.handle_generic_error(text)
685
def on_headers(self, info):
686
if 'total-size' in info:
687
self.totalSize = info['total-size']
688
if not self.accept_download_size(self.totalSize):
689
self.handle_error(_("Not enough disk space"),
690
_("%(amount)s MB required to store this video") %
691
{"amount": self.totalSize / (2 ** 20)})
693
# We should successfully download the file. Reset retryCount
694
# and accept defeat if we see an error.
695
self.restartOnError = False
696
# update shortFilename based on the headers. This will affect
697
# how we move the file once the download is finished
698
self.shortFilename = clean_filename(info['filename'])
699
if self.expectedContentType is not None:
700
ext_content_type = self.expectedContentType
702
ext_content_type = info.get('content-type')
703
self.shortFilename = check_filename_extension(self.shortFilename,
706
def on_download_error(self, error):
707
if isinstance(error, httpclient.ResumeFailed):
708
# try starting from scratch
711
self.start_new_download()
712
elif isinstance(error, httpclient.AuthorizationCanceled):
713
self.destroy_client()
715
elif self.restartOnError:
716
self.restartOnError = False
717
self.start_download()
719
self.destroy_client()
720
self.handle_network_error(error)
722
def on_download_finished(self, response):
723
self.destroy_client()
724
self.state = "finished"
725
self.endTime = clock()
726
# bug 14131 -- if there's nothing here, treat it like a temporary
728
if self.currentSize == 0:
729
self.handle_network_error(httpclient.PossiblyTemporaryError(_("no content")))
732
if self.totalSize == -1:
733
self.totalSize = self.currentSize
735
self.move_to_movies_directory()
737
self.handle_write_error(e)
740
def get_status(self):
741
data = BGDownloader.get_status(self)
742
data['dlerType'] = 'HTTP'
745
def update_stats(self):
746
"""Update the download rate and eta based on receiving length
749
if self.client is None or self.state != 'downloading':
751
stats = self.client.get_stats()
752
if stats.status_code in (200, 206):
753
self.currentSize = stats.downloaded + stats.initial_size
754
self.rate = stats.download_rate
756
self.currentSize = self.rate = 0
757
eventloop.add_timeout(self.CHECK_STATS_TIMEOUT, self.update_stats,
758
'update http downloader stats')
759
DOWNLOAD_UPDATER.queue_update(self)
762
"""Pauses the download.
764
if self.state != "stopped":
765
self.cancel_request()
766
self.state = "paused"
769
def stop(self, delete):
770
"""Stops the download and removes the partially downloaded
773
if self.state == 'finished':
776
if fileutil.isdir(self.filename):
777
fileutil.rmtree(self.filename)
779
fileutil.remove(self.filename)
783
# Cancel the request, don't keep around partially
785
self.cancel_request(remove_file=True)
787
self.state = "stopped"
790
def stop_upload(self):
791
# HTTP downloads never upload.
794
def start(self, resume=True):
795
"""Continues a paused or stopped download thread.
797
if self.state in ('paused', 'stopped', 'offline'):
798
self.state = "downloading"
799
self.start_download(resume=resume)
802
self.cancel_request()
805
class BTDownloader(BGDownloader):
806
# update fast resume every 5 minutes
807
FAST_RESUME_UPDATE_INTERVAL = 60 * 5
809
def __init__(self, url=None, item=None, restore=None):
812
self.rate = self.eta = 0
813
self.upRate = self.uploaded = 0
815
self.fastResumeData = None
817
self.channelName = None
818
self.uploadedStart = 0
819
self.restarting = False
822
self.last_fast_resume_update = clock()
823
self.metainfo_updated = self.fast_resume_data_updated = False
824
if restore is not None:
825
self.firstTime = False
826
self.restore_state(restore)
828
self.firstTime = True
829
BGDownloader.__init__(self,url,item)
830
self.run_downloader()
832
def _start_torrent(self):
834
torrent_info = lt.torrent_info(lt.bdecode(self.metainfo))
835
duplicate = TORRENT_SESSION.find_duplicate_torrent(torrent_info)
836
if duplicate is not None:
837
c = command.DuplicateTorrent(daemon.LAST_DAEMON,
838
duplicate.dlid, self.dlid)
841
self.totalSize = torrent_info.total_size()
843
if self.firstTime and not self.accept_download_size(self.totalSize):
845
_("Not enough disk space"),
846
_("%(amount)s MB required to store this video",
847
{"amount": self.totalSize / (2 ** 20)})
851
save_path = os.path.dirname(fileutil.expand_filename(self.filename))
852
if self.fastResumeData:
853
self.torrent = TORRENT_SESSION.session.add_torrent(
854
torrent_info, save_path, lt.bdecode(self.fastResumeData),
855
lt.storage_mode_t.storage_mode_allocate)
856
self.torrent.resume()
858
self.torrent = TORRENT_SESSION.session.add_torrent(
859
torrent_info, save_path, None,
860
lt.storage_mode_t.storage_mode_allocate)
862
if (lt.version_major, lt.version_minor) > (0, 13):
864
"setting libtorrent auto_managed to False")
865
self.torrent.auto_managed(False)
866
except AttributeError:
867
logging.warning("libtorrent module doesn't have "
868
"version_major or version_minor")
869
except (SystemExit, KeyboardInterrupt):
872
self.handle_error(_('BitTorrent failure'),
873
_('BitTorrent failed to startup'))
874
logging.exception("Exception thrown in _start_torrent")
876
TORRENT_SESSION.add_torrent(self)
878
def _shutdown_torrent(self):
880
TORRENT_SESSION.remove_torrent(self)
881
if self.torrent is not None:
883
self.update_fast_resume_data()
884
TORRENT_SESSION.session.remove_torrent(self.torrent, 0)
886
except (SystemExit, KeyboardInterrupt):
889
logging.exception("Error shutting down torrent")
891
def _pause_torrent(self):
893
TORRENT_SESSION.remove_torrent(self)
894
if self.torrent is not None:
896
except (SystemExit, KeyboardInterrupt):
899
logging.exception("Error pausing torrent")
901
def _resume_torrent(self):
902
if self.torrent is None:
903
self._start_torrent()
907
self.torrent.resume()
908
TORRENT_SESSION.add_torrent(self)
909
except (SystemExit, KeyboardInterrupt):
912
logging.exception("Error resuming torrent")
914
def update_status(self):
916
activity -- string specifying what's currently happening or None for
918
upRate -- upload rate in B/s
919
downRate -- download rate in B/s
920
upTotal -- total MB uploaded
921
downTotal -- total MB downloaded
922
fractionDone -- what portion of the download is completed.
923
timeEst -- estimated completion time, in seconds.
924
totalSize -- total size of the torrent in bytes
926
status = self.torrent.status()
927
self.totalSize = status.total_wanted
928
self.rate = status.download_payload_rate
929
self.upRate = status.upload_payload_rate
930
self.uploaded = status.total_payload_upload + self.uploadedStart
931
self.seeders = status.num_complete
932
self.leechers = status.num_incomplete
934
self.eta = ((status.total_wanted - status.total_wanted_done) /
935
float(status.download_payload_rate))
936
except ZeroDivisionError:
938
if status.state == lt.torrent_status.states.queued_for_checking:
939
self.activity = "waiting to check existing files"
940
elif status.state == lt.torrent_status.states.checking_files:
941
self.activity = "checking existing files"
942
elif status.state == lt.torrent_status.states.allocating:
943
self.activity = "allocating disk space"
946
self.currentSize = status.total_wanted_done
947
if ((self.state == "downloading"
948
and status.state == lt.torrent_status.states.seeding)):
949
self.move_to_movies_directory()
950
self.state = "uploading"
951
self.endTime = clock()
954
DOWNLOAD_UPDATER.queue_update(self)
956
if config.get(prefs.LIMIT_UPLOAD_RATIO):
957
if status.state == lt.torrent_status.states.seeding:
958
if ((float(self.uploaded) / self.totalSize >
959
config.get(prefs.UPLOAD_RATIO))):
962
if self.should_update_fast_resume_data():
963
self.update_fast_resume_data()
965
def should_update_fast_resume_data(self):
966
return (clock() - self.last_fast_resume_update >
967
self.FAST_RESUME_UPDATE_INTERVAL)
969
def update_fast_resume_data(self):
970
self.last_fast_resume_update = clock()
971
self.fastResumeData = lt.bencode(self.torrent.write_resume_data())
972
self.fast_resume_data_updated = True
974
def handle_error(self, shortReason, reason):
975
self._shutdown_torrent()
976
BGDownloader.handle_error(self, shortReason, reason)
978
def handle_temporary_error(self, shortReason, reason):
979
self._shutdown_torrent()
980
BGDownloader.handle_temporary_error(self, shortReason, reason)
982
def move_to_directory(self, directory):
983
if self.state in ('uploading', 'downloading'):
984
self._shutdown_torrent()
985
BGDownloader.move_to_directory(self, directory)
986
self._resume_torrent()
988
BGDownloader.move_to_directory(self, directory)
990
def restore_state(self, data):
991
self.__dict__.update(data)
992
self.rate = self.eta = 0
994
self.uploadedStart = self.uploaded
995
if self.state in ('downloading', 'uploading'):
996
self.run_downloader(done=True)
997
elif self.state == 'offline':
1000
def get_status(self):
1001
data = BGDownloader.get_status(self)
1002
data['upRate'] = self.upRate
1003
data['uploaded'] = self.uploaded
1004
if self.metainfo_updated:
1005
data['metainfo'] = self.metainfo
1006
self.metainfo_updated = False
1007
if self.fast_resume_data_updated:
1008
data['fastResumeData'] = self.fastResumeData
1009
self.fast_resume_data_updated = False
1010
data['activity'] = self.activity
1011
data['dlerType'] = 'BitTorrent'
1012
data['seeders'] = self.seeders
1013
data['leechers'] = self.leechers
1023
self.state = "paused"
1024
self.restarting = True
1025
self._pause_torrent()
1026
self.update_client()
1028
def stop(self, delete):
1029
self.state = "stopped"
1030
self._shutdown_torrent()
1031
self.update_client()
1034
if fileutil.isdir(self.filename):
1035
fileutil.rmtree(self.filename)
1037
fileutil.remove(self.filename)
1041
def stop_upload(self):
1042
self.state = "finished"
1043
self._shutdown_torrent()
1044
self.update_client()
1046
def pause_upload(self):
1047
self.state = "uploading-paused"
1048
self._shutdown_torrent()
1049
self.update_client()
1051
def start(self, resume=True):
1052
# for BT downloads, resume doesn't mean anything, so we
1054
if self.state not in ('paused', 'stopped', 'offline'):
1057
self.state = "downloading"
1059
self.retryDC.cancel()
1061
self.update_client()
1065
self._shutdown_torrent()
1066
self.update_client()
1068
def got_metainfo(self):
1069
# FIXME: If the client is stopped before a BT download gets
1070
# its metadata, we never run this. It's not a huge deal
1071
# because it only affects the incomplete filename
1072
if not self.restarting:
1074
metainfo = lt.bdecode(self.metainfo)
1075
# if we don't get valid torrent metadata back, then the
1076
# metainfo is None. treat that like a runtime error.
1078
raise RuntimeError()
1079
name = metainfo['info']['name']
1080
except RuntimeError:
1081
self.handle_corrupt_torrent()
1083
self.shortFilename = utf8_to_filename(name)
1084
self.pick_initial_filename(suffix="", torrent=True)
1085
self.update_client()
1086
self._resume_torrent()
1088
def handle_corrupt_torrent(self):
1090
_("Corrupt Torrent"),
1091
_("The torrent file at %(url)s was not valid",
1092
{"url": stringify(self.url)})
1095
def handle_metainfo(self, metainfo):
1096
self.metainfo = metainfo
1097
self.metainfo_updated = True
1100
def check_description(self, data):
1101
if len(data) > MAX_TORRENT_SIZE or data[0] != 'd':
1102
# Bailout if we get too much data or it doesn't begin with
1103
# "d" (see #12301 for details)
1104
eventloop.add_idle(self.handle_corrupt_torrent,
1105
'description check failed')
1110
def on_metainfo_download(self, info):
1111
self.handle_metainfo(info['body'])
1113
def on_metainfo_download_error(self, exception):
1114
self.handle_network_error(exception)
1116
def get_metainfo(self):
1117
if self.metainfo is None:
1118
if self.url.startswith('file://'):
1119
path = get_file_url_path(self.url)
1121
metainfoFile = open(path, 'rb')
1123
self.handle_error(_("Torrent file deleted"),
1124
_("The torrent file for this item was deleted "
1125
"outside of %(appname)s.",
1126
{"appname": config.get(prefs.SHORT_APP_NAME)}
1131
metainfo = metainfoFile.read()
1133
metainfoFile.close()
1135
self.handle_metainfo(metainfo)
1137
self.description_client = httpclient.grab_url(self.url,
1138
self.on_metainfo_download,
1139
self.on_metainfo_download_error,
1140
content_check_callback=self.check_description)
1144
def run_downloader(self, done=False):
1145
self.restarting = done
1146
self.update_client()