| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- # The contents of this file are subject to the BitTorrent Open Source License
- # Version 1.1 (the License). You may not copy or use this file, in either
- # source code or executable form, except in compliance with the License. You
- # may obtain a copy of the License at http://www.bittorrent.com/license/.
- #
- # Software distributed under the License is distributed on an AS IS basis,
- # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- # for the specific language governing rights and limitations under the
- # License.
- # written by Matt Chisholm
- from __future__ import division
- import os
- import os.path
- import atexit
- import itertools
- import webbrowser
- from copy import copy
- import logging
- import logging.handlers
- from BTL.translation import _
- import BTL.stackthreading as threading
- from BTL.platform import bttime, efs2
- from BTL.obsoletepythonsupport import set
- from BTL.yielddefer import launch_coroutine
- from BTL.defer import ThreadedDeferred, wrap_task
- from BTL.ThreadProxy import ThreadProxy
- from BTL.exceptions import str_exc
- from BTL.formatters import percentify, Size, Rate, Duration
- from BitTorrent import GetTorrent
- from BitTorrent import LaunchPath
- from BitTorrent.MultiTorrent import UnknownInfohash, TorrentAlreadyInQueue, TorrentAlreadyRunning, TorrentNotRunning
- from BitTorrent.platform import desktop
- from BitTorrent.Torrent import *
- state_dict = {("created", "stop", False): _("Paused"),
- ("created", "stop", True): _("Paused"),
- ("created", "start", False): _("Starting"),
- ("created", "start", True): _("Starting"),
- ("created", "auto", False): _("Starting"),
- ("created", "auto", True): _("Starting"),
- ("initializing", "stop", False): _("Paused"),
- ("initializing", "stop", True): _("Paused"),
- ("initializing", "start", False): _("Starting"),
- ("initializing", "start", True): _("Starting"),
- ("initializing", "auto", False): _("Starting"),
- ("initializing", "auto", True): _("Starting"),
- ("initialized", "stop", False): _("Paused"),
- ("initialized", "stop", True): _("Paused"),
- ("initialized", "start", False): _("Starting"),
- ("initialized", "start", True): _("Starting"),
- ("initialized", "auto", False): _("Queued"),
- ("initialized", "auto", True): _("Complete"),
- ("running", "stop", False): _("Downloading"),
- ("running", "stop", True): _("Seeding"),
- ("running", "start", False): _("Downloading"),
- ("running", "start", True): _("Seeding"),
- ("running", "auto", False): _("Downloading"),
- ("running", "auto", True): _("Complete"),
- ("finishing", "stop", False): _("Finishing"),
- ("finishing", "stop", True): _("Finishing"),
- ("finishing", "start", False): _("Finishing"),
- ("finishing", "start", True): _("Finishing"),
- ("finishing", "auto", False): _("Finishing"),
- ("finishing", "auto", True): _("Finishing"),
- ("failed", "stop", False): _("Error"),
- ("failed", "stop", True): _("Error"),
- ("failed", "start", False): _("Error"),
- ("failed", "start", True): _("Error"),
- ("failed", "auto", False): _("Error"),
- ("failed", "auto", True): _("Error"),}
- def ip_sort(a_str,b_str):
- """Fast IP address sorting function"""
- for a,b in itertools.izip(a_str.split('.'), b_str.split('.')):
- if a == b:
- continue
- if len(a) == len(b):
- return cmp(a,b)
- return cmp(int(a), int(b))
- return 0
- def find_dir(path):
- if os.path.isdir(path):
- return path
- directory, garbage = os.path.split(path)
- while directory:
- if os.access(directory, os.F_OK) and os.access(directory, os.W_OK):
- return directory
- directory, garbage = os.path.split(directory)
- if garbage == '':
- break
- return None
- def smart_dir(path):
- path = find_dir(path)
- if path is None:
- path = desktop
- return path
- if os.name == 'nt':
- disk_term = _("drive")
- elif os.name == 'posix' and os.uname()[0] == 'Darwin':
- disk_term = _("volume")
- else:
- disk_term = _("disk")
- class BasicTorrentObject(object):
- """Object for holding all information about a torrent"""
- def __init__(self, torrent):
- self.torrent = torrent
- self.pending = None
- self.infohash = torrent.metainfo.infohash
- self.metainfo = torrent.metainfo
- self.destination_path = torrent.destination_path
- self.working_path = torrent.working_path
- self.state = torrent.state
- self.policy = torrent.policy
- self.completed = torrent.completed
- self.priority = torrent.priority
- self.completion = None
- self.piece_states = None
- self.uptotal = 0
- self.downtotal = 0
- self.up_down_ratio = 0
- self.peers = 0
- self.dead = False
- self.statistics = {}
- self.handler = logging.handlers.MemoryHandler(0) # capacity is ignored
- logging.getLogger("core.MultiTorrent." + repr(self.infohash)).addHandler(self.handler)
- def update(self, torrent, statistics):
- self.torrent = torrent
- self.statistics = statistics
- self.destination_path = torrent.destination_path
- self.working_path = torrent.working_path
- self.state = torrent.state
- self.policy = torrent.policy
- self.completed = torrent.completed
- self.priority = statistics['priority']
- self.completion = statistics['fractionDone']
- self.piece_states = statistics['pieceStates']
- self.uptotal += statistics.get('upTotal' , 0)
- self.downtotal += statistics.get('downTotal', 0)
- try:
- self.up_down_ratio = self.uptotal / self.torrent.metainfo.total_bytes
- except ZeroDivisionError:
- self.up_down_ratio = 0
- self.peers = statistics.get('numPeers', 0)
- def wants_peers(self):
- return True
- def wants_files(self):
- return self.metainfo.is_batch
- def clean_up(self):
- if self.dead:
- return
- self.dead = True
- del self.torrent
- del self.metainfo
- logging.getLogger("core.MultiTorrent." + repr(self.infohash)).removeHandler(self.handler)
- class BasicApp(object):
- torrent_object_class = BasicTorrentObject
- def __init__(self, config):
- self.started = 0
- self.multitorrent = None
- self.config = config
- self.torrents = {}
- self.external_torrents = []
- self.installer_to_launch_at_exit = None
- self.logger = logging.getLogger('UI')
- self.logger.setLevel(logging.INFO)
- self.next_autoupdate_nag = bttime()
- def gui_wrap(_f, *args, **kwargs):
- f(*args, **kwargs)
- self.gui_wrap = gui_wrap
- self.open_external_torrents_deferred = None
- def quit(self):
- if self.doneflag:
- self.doneflag.set()
- def visit_url(self, url, callback=None):
- """Visit a URL in the user's browser"""
- t = threading.Thread(target=self._visit_url, args=(url, callback))
- t.start()
- def _visit_url(self, url, callback=None):
- """Visit a URL in the user's browser non-blockingly"""
- webbrowser.open(url)
- if callback:
- self.gui_wrap(callback)
- def open_torrent_arg(self, path):
- """Open a torrent from path (URL, file) non-blockingly"""
- df = ThreadedDeferred(self.gui_wrap, GetTorrent.get_quietly, path)
- return df
- def publish_torrent(self, torrent, publish_path):
- df = self.open_torrent_arg(torrent)
- yield df
- try:
- metainfo = df.getResult()
- except GetTorrent.GetTorrentException:
- self.logger.exception("publish_torrent failed")
- return
- df = self.multitorrent.create_torrent(metainfo, efs2(publish_path), efs2(publish_path))
- yield df
- df.getResult()
- def open_torrent_arg_with_callbacks(self, path):
- """Open a torrent from path (URL, file) non-blockingly, and
- call the appropriate GUI callback when necessary."""
- def errback(f):
- exc_type, value, tb = f.exc_info()
- if issubclass(exc_type, GetTorrent.GetTorrentException):
- self.logger.critical(str_exc(value))
- else:
- self.logger.error("open_torrent_arg_with_callbacks failed",
- exc_info=f.exc_info())
- def callback(metainfo):
- def open(metainfo):
- df = self.multitorrent.torrent_known(metainfo.infohash)
- yield df
- known = df.getResult()
- if known:
- self.torrent_already_open(metainfo)
- else:
- df = self.open_torrent_metainfo(metainfo)
- if df is not None:
- yield df
- try:
- df.getResult()
- except TorrentAlreadyInQueue:
- pass
- except TorrentAlreadyRunning:
- pass
- launch_coroutine(self.gui_wrap, open, metainfo)
- df = self.open_torrent_arg(path)
- df.addCallback(callback)
- df.addErrback(errback)
- return df
- def append_external_torrents(self, *a):
- """Append external torrents (such as those specified on the
- command line) so that they can be processed (for save paths,
- error reporting, etc.) once the GUI has started up."""
- self.external_torrents.extend(a)
- def _open_external_torrents(self):
- """Open torrents added externally (on the command line before
- startup) in a non-blocking yet serial way."""
- while self.external_torrents:
- arg = self.external_torrents.pop(0)
- df = self.open_torrent_arg(arg)
- yield df
- try:
- metainfo = df.getResult()
- except GetTorrent.GetTorrentException:
- self.logger.exception("Failed to get torrent")
- continue
- if metainfo is not None:
- # metainfo may be none if IE passes us a path to a
- # file in its cache that has already been deleted
- # because it came from a website which set
- # Pragma:No-Cache on it.
- # See GetTorrent.get_quietly().
- df = self.multitorrent.torrent_known(metainfo.infohash)
- yield df
- known = df.getResult()
- if known:
- self.torrent_already_open(metainfo)
- else:
- df = self.open_torrent_metainfo(metainfo)
- if df is not None:
- yield df
- try:
- df.getResult()
- except TorrentAlreadyInQueue:
- pass
- except TorrentAlreadyRunning:
- pass
- self.open_external_torrents_deferred = None
- def open_external_torrents(self):
- """Open torrents added externally (on the command line before startup)."""
- if self.open_external_torrents_deferred is None and \
- len(self.external_torrents):
- self.open_external_torrents_deferred = launch_coroutine(self.gui_wrap, self._open_external_torrents)
- def callback(*a):
- self.open_external_torrents_deferred = None
- def errback(f):
- callback()
- self.logger.error("open_external_torrents failed:",
- exc_info=f.exc_info())
- self.open_external_torrents_deferred.addCallback(callback)
- self.open_external_torrents_deferred.addErrback(errback)
- def torrent_already_open(self, metainfo):
- """Tell the user."""
- raise NotImplementedError('BasicApp.torrent_already_open() not implemented')
- def open_torrent_metainfo(self, metainfo):
- """Get a valid save path from the user, and then tell
- multitorrent to create a new torrent from metainfo."""
- raise NotImplementedError('BasicApp.open_torrent_metainfo() not implemented')
- def launch_torrent(self, infohash):
- """Launch the torrent contents according to operating system."""
- if infohash in self.torrents:
- torrent = self.torrents[infohash]
- if torrent.metainfo.is_batch:
- LaunchPath.launchdir(torrent.working_path)
- else:
- LaunchPath.launchfile(torrent.working_path)
- def launch_torrent_folder(self, infohash):
- """Launch the torrent location according to operating system."""
- if infohash in self.torrents:
- torrent = self.torrents[infohash]
- if torrent.metainfo.is_batch:
- LaunchPath.launchdir(torrent.working_path)
- else:
- path, file = os.path.split(torrent.working_path)
- LaunchPath.launchdir(path)
- def launch_installer_at_exit(self):
- LaunchPath.launchfile(self.installer_to_launch_at_exit)
- def do_log(self, severity, text):
- raise NotImplementedError('BasicApp.do_log() not implemented')
- def attach_multitorrent(self, multitorrent, doneflag):
- self.multitorrent = multitorrent
- self.multitorrent_doneflag = doneflag
- self.rawserver = multitorrent.obj.rawserver
- self.multitorrent.initialize_torrents()
- def init_updates(self):
- """Make status request at regular intervals."""
- raise NotImplementedError('BasicApp.init_updates() not implemented')
- def make_statusrequest(self, event = None):
- """Make status request."""
- df = launch_coroutine(self.gui_wrap, self.update_status)
- def errback(f):
- self.logger.error("update_status failed",
- exc_info=f.exc_info())
- df.addErrback(errback)
- return True
- def _thread_proxy(self, obj):
- return ThreadProxy(obj,
- self.gui_wrap,
- wrap_task(self.rawserver.external_add_task))
- def update_single_torrent(self, infohash):
- torrent = self.torrents[infohash]
- df = self.multitorrent.torrent_status(infohash,
- torrent.wants_peers(),
- torrent.wants_files()
- )
- yield df
- try:
- core_torrent, statistics = df.getResult()
- except UnknownInfohash:
- # looks like it's gone now
- if infohash in self.torrents:
- self._do_remove_torrent(infohash)
- else:
- # the infohash might have been removed from torrents
- # while we were yielding above, so we need to check
- if infohash in self.torrents:
- core_torrent = self._thread_proxy(core_torrent)
- torrent.update(core_torrent, statistics)
- self.update_torrent(torrent)
- def update_status(self):
- """Update torrent information based on the results of making a
- status request."""
- df = self.multitorrent.get_torrents()
- yield df
- torrents = df.getResult()
- infohashes = set()
- au_torrents = {}
- for torrent in torrents:
- torrent = self._thread_proxy(torrent)
- infohashes.add(torrent.metainfo.infohash)
- if torrent.metainfo.infohash not in self.torrents:
- if self.config.get('show_hidden_torrents') or not torrent.hidden:
- # create new torrent widget
- to = self.new_displayed_torrent(torrent)
- if torrent.is_auto_update:
- au_torrents[torrent.metainfo.infohash] = torrent
- for infohash, torrent in copy(self.torrents).iteritems():
- # remove nonexistent torrents
- if infohash not in infohashes:
- self._do_remove_torrent(infohash)
- total_completion = 0
- total_bytes = 0
- for infohash, torrent in copy(self.torrents).iteritems():
- # update existing torrents
- df = self.multitorrent.torrent_status(infohash,
- torrent.wants_peers(),
- torrent.wants_files()
- )
- yield df
- try:
- core_torrent, statistics = df.getResult()
- except UnknownInfohash:
- # looks like it's gone now
- if infohash in self.torrents:
- self._do_remove_torrent(infohash)
- else:
- # the infohash might have been removed from torrents
- # while we were yielding above, so we need to check
- if infohash in self.torrents:
- core_torrent = self._thread_proxy(core_torrent)
- torrent.update(core_torrent, statistics)
- self.update_torrent(torrent)
- if statistics['fractionDone'] is not None:
- amount_done = statistics['fractionDone'] * torrent.metainfo.total_bytes
- total_completion += amount_done
- total_bytes += torrent.metainfo.total_bytes
- all_completed = False
- if total_bytes == 0:
- average_completion = 0
- else:
- average_completion = total_completion / total_bytes
- if total_completion == total_bytes:
- all_completed = True
- df = self.multitorrent.auto_update_status()
- yield df
- available_version, installable_version, delay = df.getResult()
- if available_version is not None:
- if installable_version is None:
- self.notify_of_new_version(available_version)
- else:
- if self.installer_to_launch_at_exit is None:
- atexit.register(self.launch_installer_at_exit)
- if installable_version not in au_torrents:
- df = self.multitorrent.get_torrent(installable_version)
- yield df
- torrent = df.getResult()
- torrent = ThreadProxy(torrent, self.gui_wrap)
- else:
- torrent = au_torrents[installable_version]
- self.installer_to_launch_at_exit = torrent.working_path
- if bttime() > self.next_autoupdate_nag:
- self.prompt_for_quit_for_new_version(available_version)
- self.next_autoupdate_nag = bttime() + delay
- def get_global_stats(mt):
- stats = {}
- u, d = mt.get_total_rates()
- stats['total_uprate'] = Rate(u)
- stats['total_downrate'] = Rate(d)
- u, d = mt.get_total_totals()
- stats['total_uptotal'] = Size(u)
- stats['total_downtotal'] = Size(d)
- torrents = mt.get_visible_torrents()
- running = mt.get_visible_running()
- stats['num_torrents'] = len(torrents)
- stats['num_running_torrents'] = len(running)
- stats['num_connections'] = 0
- for t in torrents:
- stats['num_connections'] += t.get_num_connections()
- try:
- stats['avg_connections'] = (stats['num_connections'] /
- stats['num_running_torrents'])
- except ZeroDivisionError:
- stats['avg_connections'] = 0
- stats['avg_connections'] = "%.02f" % stats['avg_connections']
- return stats
- df = self.multitorrent.call_with_obj(get_global_stats)
- yield df
- global_stats = df.getResult()
- yield average_completion, all_completed, global_stats
- def _update_status(self, total_completion):
- raise NotImplementedError('BasicApp._update_status() not implemented')
- def new_displayed_torrent(self, torrent):
- """Tell the UI that it should draw a new torrent."""
- torrent_object = self.torrent_object_class(torrent)
- self.torrents[torrent.metainfo.infohash] = torrent_object
- return torrent_object
- def torrent_removed(self, infohash):
- """Tell the GUI that a torrent has been removed, by it, or by
- multitorrent."""
- raise NotImplementedError('BasicApp.torrent_removed() removing missing torrents not implemented')
- def update_torrent(self, torrent_object):
- """Tell the GUI to update a torrent's info."""
- raise NotImplementedError('BasicApp.update_torrent() updating existing torrents not implemented')
- def notify_of_new_version(self, version):
- print 'got auto_update_status', version
- pass
- def prompt_for_quit_for_new_version(self, version):
- print 'got new version', version
- pass
- # methods that are used to send commands to MultiTorrent
- def send_config(self, option, value, infohash=None):
- """Tell multitorrent to set a config item."""
- self.config[option] = value
- if self.multitorrent:
- self.multitorrent.set_option(option, value, infohash)
- def remove_infohash(self, infohash, del_files=False):
- """Tell multitorrent to remove a torrent."""
- df = self.multitorrent.remove_torrent(infohash, del_files=del_files)
- yield df
- try:
- df.getResult()
- except KeyError:
- pass # it was already gone, who cares
- if infohash in self.torrents:
- self._do_remove_torrent(infohash)
- def _do_remove_torrent(self, infohash):
- self.torrent_removed(infohash)
- torrent_object = self.torrents.pop(infohash)
- torrent_object.clean_up()
- def set_file_priority(self, infohash, filenames, dowhat):
- """Tell multitorrent to set file priorities."""
- for f in filenames:
- self.multitorrent.set_file_priority(infohash, f, dowhat)
- def stop_torrent(self, infohash, pause=False):
- """Tell multitorrent to stop a torrent."""
- torrent = self.torrents[infohash]
- if (torrent and torrent.pending == None):
- torrent.pending = "stop"
- df = self.multitorrent.set_torrent_policy(infohash, "stop")
- yield df
- try:
- df.getResult()
- except TorrentNotRunning:
- pass
- if torrent.state == "running":
- df = self.multitorrent.stop_torrent(infohash, pause=pause)
- yield df
- torrent.state = df.getResult()
- torrent.pending = None
- yield True
- def start_torrent(self, infohash):
- """Tell multitorrent to start a torrent."""
- torrent = self.torrents[infohash]
- if (torrent and torrent.pending == None and
- torrent.state in ["failed", "initialized"]):
- torrent.pending = "start"
- if torrent.state == "failed":
- df = self.multitorrent.reinitialize_torrent(infohash)
- yield df
- df.getResult()
- df = self.multitorrent.set_torrent_policy(infohash, "auto")
- yield df
- df.getResult()
- torrent.pending = None
- yield True
- def force_start_torrent(self, infohash):
- torrent = self.torrents[infohash]
- if (torrent and torrent.pending == None):
- torrent.pending = "force start"
- df = self.multitorrent.set_torrent_policy(infohash, "start")
- yield df
- df.getResult()
- if torrent.state in ["failed", "initialized"]:
- if torrent.state == "failed":
- df = self.multitorrent.reinitialize_torrent(infohash)
- yield df
- df.getResult()
- df = self.multitorrent.start_torrent(infohash)
- yield df
- try:
- torrent.state = df.getResult()
- except TorrentAlreadyRunning:
- torrent.state = "running"
- torrent.pending = None
- yield True
- def no_op(self):
- pass
- def external_command(self, action, *datas):
- """For communication via IPC"""
- datas = [ d.decode('utf-8') for d in datas ]
- if action == 'start_torrent':
- assert len(datas) == 1, 'incorrect data length'
- self.append_external_torrents(*datas)
- self.logger.info('got external_command:start_torrent: "%s"' % datas[0])
- # this call does Ye Olde Threadede Deferrede:
- self.open_external_torrents()
- elif action == 'publish_torrent':
- self.logger.info('got external_command:publish_torrent: "%s" as "%s"' % datas)
- launch_coroutine(self.gui_wrap, self.publish_torrent, datas[0], datas[1])
- elif action == 'show_error':
- assert len(datas) == 1, 'incorrect data length'
- self.logger.error(datas[0])
- elif action == 'no-op':
- self.no_op()
- self.logger.info('got external_command: no-op')
- else:
- self.logger.warning('got unknown external_command: %s' % str(action))
- # fun.
- #code = action + ' '.join(datas)
- #self.logger.warning('eval: %s' % code)
- #exec code
|