home *** CD-ROM | disk | FTP | other *** search
Text File | 2007-03-29 | 42.0 KB | 1,344 lines |
- #!/usr/bin/python
-
- # BitTorrent related modules.
- import BitTorrent.download, BitTorrent.bencode
-
- # Various system and utility modules.
- import os, os.path, threading, sha, sys, time, re
-
- # GTK+ and GNOME related modules.
- import gobject, gtk, gtk.glade, gnome, gnome.ui, gconf
- try:
- # The new module name
- import gnomevfs
- except:
- import gnome.vfs
- gnomevfs = gnome.vfs
-
- # Gettext
- import gettext
- _ = gettext.gettext
-
- # The name of this program.
- app_name = 'gnome-btdownload'
-
- # The version of this program.
- app_version = '0.0.25'
-
- # A hack that is set to a value that is the largest possible BitTorrent meta
- # file. This is passed to get_url to pull the entire (hopefully) meta file into
- # memory. I do this to prevent huge files from being loaded into memory.
- max_torrent_size = 0x400000 # 4 MB
-
- # From RFC 2396, the regular expression for well-formed URIs
- rfc_2396_uri_regexp = r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?"
-
- # If assigned, called with:
- # type: string
- # Describes the classification of the file to infer where it's
- # stored.
- #
- # Currently valid options:
- # * 'data'
- # filename: string
- # The name of the file for which to look.
- # sub: string or None
- # A potential sub-directory to check (and prefer) for the file.
- locate_file = None
-
- # The 'GnomeProgram' for this process.
- gnome_program = gnome.program_init(app_name, app_version)
-
- # Makes sure a URI is fully qualified and return the result. Return None if it
- # isn't a URI.
- def fix_up_uri(path):
- try:
- # 2 == GNOME_VFS_MAKE_URI_DIR_CURRENT, but isn't exported to python.
- # This does not appear to support relative URIs, but does
- # support relative paths, at least.
- return gnomevfs.URI(gnomevfs.make_uri_from_input_with_dirs(path, 2))
- except:
- return None
-
- # Checks if a path exists. This is still around from when this was a 'virtual'
- # function.
- def path_exists(path):
- return gnomevfs.exists(path)
-
- # GNOME wrapper
- def can_show_path(path):
- return True
- def show_path(path):
- gnome.url_show(str(fix_up_uri(path)))
-
- # GNOME wrapper
- def get_home_dir():
- return os.path.expanduser('~')
-
- # Wrapper
- def get_config_dir():
- home_dir = get_home_dir()
-
- if path_exists(os.path.join(home_dir, '.gnome2')):
- return os.path.join(home_dir, '.gnome2', app_name)
- else:
- return os.path.join(home_dir, '.'+app_name)
- def make_config_dir():
- home_dir = get_home_dir()
- config_dir = None
-
- if path_exists(os.path.join(home_dir, '.gnome2')):
- config_dir = os.path.join(home_dir, '.gnome2', app_name)
- else:
- config_dir = os.path.join(home_dir, '.'+app_name)
-
- if not path_exists(config_dir):
- gnomevfs.make_directory(config_dir, 00750)
-
- cache_dir = os.path.join(config_dir, 'cache')
-
- if not path_exists(cache_dir):
- gnomevfs.make_directory(cache_dir, 00777)
-
- # Disabled until gnome_program.locate_file is exported by gnome-python...
- #def locate_file(type filename, sub):
- # FIXME gnome_program.locate_file(gnome.FILE_DOMAIN_APP_DATADIR, filename, True)
-
- # Load at most read_bytes from a URI and return the result.
- def get_url(uri, read_bytes):
- content = ''
- total_size = 0
-
- try:
- handle = gnomevfs.open(uri, gnomevfs.OPEN_READ)
- except Exception, e:
- print >> sys.stderr, 'gnomevfs.open failed with: url('+str(uri)+') e: '+str(e)
- return None
- try:
- tmp = handle.read(read_bytes)
- tmp_size = len(tmp)
-
- while(tmp_size < read_bytes - total_size):
- content += tmp
- total_size += tmp_size
-
- tmp = handle.read(read_bytes - total_size)
- tmp_size = len(tmp)
- except gnomevfs.EOFError:
- pass
-
- return content
-
- # Fallback wrapper
- if not locate_file:
- def fallback_locate_attempt_prefixes(path):
- prefixes = ['', 'usr/', 'usr/local/']
-
- # Try them locally
- for prefix in prefixes:
- if os.path.exists(prefix + path):
- return prefix + path;
-
- # Try them from root
- for prefix in prefixes:
- if os.path.exists('/' + prefix + path):
- return '/' + prefix + path;
-
- return None
-
- def fallback_locate_attempt(prefix, path, sub, filename):
- if sub:
- prefix_path_sub_file = fallback_locate_attempt_prefixes(prefix + '/' + path + '/' + sub + '/' + filename)
- if prefix_path_sub_file:
- return prefix_path_sub_file
-
- prefix_path_file = fallback_locate_attempt_prefixes(prefix + '/' + path + '/' + filename)
- if prefix_path_file:
- return prefix_path_file
-
- return None
-
- def fallback_locate_file(type, filename, sub=None):
- if type == 'data':
- # Sources: Common Common FreeBSD FreeBSD
- prefixes = ['share', 'local/share', 'share/gnome', 'X11R6/share/gnome']
-
- for prefix in prefixes:
- result = fallback_locate_attempt(prefix, app_name, sub, filename)
- if result:
- return result
-
- print >> sys.stderr, _("Couldn't locate file, will probably explode...")
- return None
-
- if not locate_file:
- locate_file = fallback_locate_file
-
- # Converts a number of seconds into a short displayable string.
- def fmt_time_short(all):
- minutes = int(all / 60)
- seconds = all - (minutes * 60)
-
- return '%0.2u.%0.2u' % (minutes, seconds)
-
- # Converts a number of seconds into a precise, but verbose displayable string.
- def fmt_time_long_precise_verbose(seconds):
- seconds = int(seconds)
-
- days = seconds / (60 * 60 * 24)
- seconds -= days * (60 * 60 * 24)
-
- hours = seconds / (60 * 60)
- seconds -= hours * (60 * 60)
-
- minutes = seconds / 60
- seconds -= minutes * 60
-
- # FIXME Kind-of convoluted, not really locale friendly...
- def create_listing(items):
- def append_listing(listing, singular, plural, count, index, length):
- initial = not listing
-
- if listing or count != 0 or index+1 == length:
- listing += str(count) + ' '
-
- if count == 1:
- listing += singular
- else:
- listing += plural
-
-
- if index+1 < length:
- if index+2 == length:
- if initial:
- listing += ' and '
- else:
- listing += ', and '
- else:
- listing += ', '
-
- return listing
-
- listing = ''
-
- for index, item in zip(range(0,len(items)), items):
- listing = append_listing(listing, item[0][0], item[0][1], item[1], index, len(items))
-
- return listing
-
- return create_listing((
- (('day', 'days'), days),
- (('hour', 'hours'), hours),
- (('minute', 'minutes'), minutes),
- (('second', 'seconds'), seconds)
- ))
-
- # Converts a number of seconds into a rough estimate
- def fmt_time_long_estimate(seconds):
- seconds = int(seconds)
-
- days = seconds / (60 * 60 * 24)
- seconds -= days * (60 * 60 * 24)
-
- hours = seconds / (60 * 60)
- seconds -= hours * (60 * 60)
-
- minutes = seconds / 60
- seconds -= minutes * 60
-
- if days > 1:
- return _('About %i days') % days
- elif days == 1:
- return _('About %i day') % days
- elif hours > 1:
- return _('About %i hours') % hours
- elif hours == 1:
- return _('About %i hour') % hours
- elif minutes > 1:
- return _('About %i minutes') % minutes
- elif minutes == 1:
- return _('About %i minute') % minutes
- elif seconds > 1:
- return _('About %i seconds') % seconds
- elif seconds == 1:
- return _('About %i second') % seconds
- elif seconds <= 0:
- return _('About %i seconds') % seconds
-
- fmt_time_long = fmt_time_long_estimate
-
- # A GNOME HIG compliant error dialog wrapper.
- class GtkHigErrorDialog:
- glade_xml = None
- dialog = None
-
- def run(self):
- self.dialog.run()
- self.dialog.destroy()
-
- def __init__(self, text, subtext='', modal=False):
- self.glade_xml = gtk.glade.XML(locate_file('data', 'errdiag.glade', 'glade'))
-
- self.dialog = self.glade_xml.get_widget('dialog_hig_error')
-
- # It's no longer HIG-y to leave dialogs with empty titles. Oh,
- # and no window managers play along either. So, we'll set the
- # title to the error message for now.
- self.dialog.set_title(text)
-
- self.glade_xml.get_widget('label_text').set_markup('<b>' + str(text) + '</b>')
- self.glade_xml.get_widget('label_subtext').set_markup(subtext)
-
- self.dialog.set_modal(modal)
-
- # A GNOME HIG complient "continue session?" dialog wrapper.
- class GtkHigContinueSessionDialog:
- glade_xml = None
- dialog = None
-
- def run(self):
- ret = self.dialog.run()
-
- self.dialog.destroy()
-
- if ret == gtk.RESPONSE_ACCEPT:
- return True
- elif ret == gtk.RESPONSE_CANCEL:
- return False
- else:
- return None
-
- def __init__(self, previous, modal=False):
- self.glade_xml = gtk.glade.XML(locate_file('data', 'contdiag.glade', 'glade'))
-
- self.dialog = self.glade_xml.get_widget('dialog_hig_continue')
-
- self.glade_xml.get_widget('label_previous').set_markup(previous)
-
- self.dialog.set_modal(modal)
-
- # A base wrapper for open and save dialogs.
- class GtkFileActionDialog:
- dialog = None
-
- def run(self):
- ret = None
-
- if self.dialog.run() == gtk.RESPONSE_ACCEPT:
- ret = self.dialog.get_filename()
-
- self.dialog.destroy()
-
- return ret
-
- def __init__(self, title, action, buttons, filters=None, default_name=None, default_path=None, modal=False, multiple=False, localonly=False):
- self.dialog = gtk.FileChooserDialog(title, None, action, buttons)
-
- self.dialog.set_local_only(localonly)
- self.dialog.set_select_multiple(multiple)
-
- if default_path:
- self.dialog.set_current_folder(default_path)
-
- if default_name and (action == gtk.FILE_CHOOSER_ACTION_SAVE or action == gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER):
- self.dialog.set_current_name(default_name)
-
- if filters:
- default_filter = None
-
- for default, name, type, etc in filters:
- filter = gtk.FileFilter()
-
- filter.set_name(name)
-
- if type == 'mime':
- filter.add_mime_type(etc)
- elif type == 'pattern':
- filter.add_pattern(etc)
- elif type == 'custom':
- filter.add_custom(etc)
-
- self.dialog.add_filter(filter)
-
- if default:
- default_filter = filter
-
- all_filter = gtk.FileFilter()
- all_filter.set_name(_('All files'))
- all_filter.add_pattern('*')
-
- if not default_filter:
- default_filter = all_filter
-
- self.dialog.add_filter(all_filter)
- self.dialog.set_filter(default_filter)
-
- self.dialog.set_modal(modal)
- self.dialog.show()
-
- # A wrapper for the open file dialog.
- class GtkFileOpenDialog(GtkFileActionDialog):
- def __init__(self, title, folder=False, filters=None, default=None, modal=False, multiple=False, localonly=False):
- action = gtk.FILE_CHOOSER_ACTION_OPEN
-
- if folder:
- action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
-
- GtkFileActionDialog.__init__(self, title, action, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT), filters, default, None, modal, multiple, localonly)
-
- # A wrapper for the save file dialog.
- class GtkFileSaveDialog(GtkFileActionDialog):
- def __init__(self, title, folder=False, filters=None, default_name=None, default_path=None, modal=False, multiple=False, localonly=False):
- action = gtk.FILE_CHOOSER_ACTION_SAVE
-
- if folder:
- action = gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER
-
- GtkFileActionDialog.__init__(self, title, action, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT), filters, default_name, default_path, modal, multiple, localonly)
-
- # Manages a BitTorrent session's state into something consistent.
- class BtState:
- # Handles the arguments passed to a BtState at initialization based
- # upon command line arguments.
- class Args:
- def __init__(self, args, bt_swa=(
- '-i',
- '--ip',
- '--bind',
- '--keepalive_interval',
- '--download_slice_size',
- '--request_backlog',
- '--max_message_length',
- '--timeout',
- '--timeout_check_interval',
- '--max_slice_length',
- '--max_rate_recalculate_interval',
- '--max_rate_period',
- '--upload_rate_fudge',
- '--display_interval',
- '--rerequest_interval',
- '--http_timeout',
- '--snub_time',
- '--spew',
- '--check_hashes',
- '--report_hash_failures',
- '--rarest_first_priority_cutoff'
- )):
- self.swa_args = []
- self.path_origin = None
- self.path_output = None
- self.min_port = None
- self.max_port = None
- self.max_uploads = None
- self.max_upload_rate = None
- self.torrent_file = None
- self.torrent_info = None
-
- # The number of arguments following the current one to ignore.
- ignore = 0
-
- for i in range(0,len(args)):
- # If we're ignoring this argument, skip it.
- if ignore > 0:
- ignore -= 1
- continue
-
- if args[i] == '--saveas':
- # Use the value to know where to save the session.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.set_path_output(args[i+1])
- elif args[i] == '--responsefile' or args[i] == '--url':
- # Use the value to know where the meta
- # file is located.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- # Convert "--responsefile [path]" into
- # "--url [uri]" and get a suggested
- # path_output if possible or needed.
- if i+1 < len(args):
- self.set_path_origin(fix_up_uri(args[i+1]))
- elif args[i] == '--max_uploads':
- # Use the value for the maximum number
- # of peers to upload to.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.max_uploads = int(args[i+1])
- elif args[i] == '--max_upload_rate':
- # Use the value for the maximum rate in
- # kbps to upload to peers
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.max_upload_rate = float(args[i+1])
- elif args[i] == '--minport':
- # Use the value for the minimum port in
- # the port range to use.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.min_port = int(args[i+1])
- elif args[i] == '--maxport':
- # Use the value for the maximum port in
- # the port range to use.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.max_port = int(args[i+1])
- elif args[i] in bt_swa:
- # This is some BitTorrent command line
- # switch, pass it on.
-
- # Ignore the next argument, since we're
- # going to use it now.
- ignore = 1
-
- if i+1 < len(args):
- self.swa_args.append(args[i])
- self.swa_args.append(args[i+1])
- elif re.match(r"(--)(.+)", args[i]):
- # This is some random command line
- # switch, ignore it.
-
- # Ignore the next argument, as well.
- ignore = 1
- else:
- # Assume any stray argument is the
- # path_origin as if passed to
- # --reponsefile or --url if it's a
- # valid URI.
-
- if not self.path_origin and i < len(args):
- fixed = fix_up_uri(args[i])
-
- if fixed:
- self.set_path_origin(fixed)
-
- # Returns the meta file's contents
- def get_torrent_file(self):
- if self.path_origin:
- if not self.torrent_file:
- try:
- self.torrent_file = get_url(self.path_origin, max_torrent_size)
- except:
- pass
-
- return self.torrent_file
-
- # Returns the meta file's information.
- def get_torrent_info(self):
- torrent_file = self.get_torrent_file()
-
- if torrent_file:
- if not self.torrent_info:
- try:
- self.torrent_info = BitTorrent.bencode.bdecode(torrent_file)
- except:
- pass
-
- return self.torrent_info
-
- # Suggest an output path from the input path and/or the meta
- # file.
- def find_suggested_path_output(self):
- suggested_path_output = None
- torrent_info = self.get_torrent_info()
-
- if torrent_info:
- suggested_path_output = torrent_info['info']['name']
- elif self.path_origin:
- suggested_path_output = self.path_origin.short_name
-
- if suggested_path_output[-len('.torrent'):] == '.torrent':
- suggested_path_output = suggested_path_output[:-len('.torrent')]
-
- return suggested_path_output
-
- # Tells if the torrent tracks multiple files by reading the
- # meta file.
- def test_has_multiple_files(self):
- has_multiple_files = False
- torrent_info = self.get_torrent_info()
-
- if torrent_info:
- has_multiple_files = torrent_info['info'].has_key('files')
-
- return has_multiple_files
-
- # Set the path_origin and update anything that might depend
- # upon it. Expects a GnomeVFSURI.
- def set_path_origin(self, path_origin):
- # Invalidate information retreived from any old
- # path_origin, just in-case
- self.torrent_file = None
- self.torrent_info = None
-
- self.path_origin = path_origin
-
- # Set the path_output.
- def set_path_output(self, path_output):
- self.path_output = path_output
-
- # Set the min_port and max_port.
- def set_ports(self, min_port, max_port):
- self.min_port = min_port
- self.max_port = max_port
-
- # Retreive the entire argument string
- def get_args(self):
- args = []
-
- if self.path_origin:
- args.append('--url')
- args.append(str(self.path_origin))
-
- if self.path_output:
- args.append('--saveas')
- args.append(self.path_output)
-
- if self.min_port and self.max_port:
- args.append('--minport')
- args.append(str(self.min_port))
- args.append('--maxport')
- args.append(str(self.max_port))
-
- return args + self.swa_args
-
- def __init__(self, args):
- # BitTorrent module related information
- self.path_origin = args.path_origin # The URI of the meta file
- self.size_total = 0 # Total bytes of the download
- self.args = args.get_args() # The command line arguments to pass to the BitTorrent module's download
- # Local information
- self.path_output = '' # The path to which the session is being downloaded.
- # Transfer information
- self.done = False # True if the download portion of the session is complete
- self.event = None # Event used to flag the BitTorrent module to kill the session
- self.thread = None # Thread running the BitTorrent module's download
- self.activity = None # What the session is doing at the moment
- self.time_begin = 0.0 # When the current session began
- self.time_remaining = 0.0 # Estimated time remaining for the download to complete
- self.dl_rate = 0.0 # The current rate of download in bytes/sec
- self.dl_amount = 0 # Bytes downloaded from the current session
- self.dl_pre_amount = 0 # Bytes downloaded from previous sessions
- self.ul_rate = 0.0 # The current rate of upload in bytes/sec
- self.ul_amount = None # Bytes uploaded from the current session (None for unknown)
- self.ul_pre_amount = 0 # Bytes uploaded from previous sessions
- self.max_uploads = 0 # Maximum number of peers to upload to
- self.max_upload_rate = 0.0 # Maximum total bytes/sec to upload at once
- # Implementation information
- self.params = {} # Parameters passed from the BitTorrent module
- self.params_pounce = True # If current settings still need to be reflected in the session
-
- # Return the corrected-for-multiple-sessions downloaded amount in bytes.
- def get_dl_amount(self):
- if self.activity == 'checking existing file':
- return self.dl_amount
- else:
- return self.dl_amount + self.dl_pre_amount
-
- # Return the corrected-for-multiple-sessions uploaded amount in bytes.
- def get_ul_amount(self):
- if self.ul_amount:
- return self.ul_amount + self.ul_pre_amount
- elif self.ul_pre_amount > 0:
- return self.ul_pre_amount
- else:
- return None
-
- # Pseudo-callback to update state when 'file' BitTorrent callback is called.
- def file(self, default, size, saveas, dir):
- self.done = False
- self.size_total = size
-
- if saveas:
- self.path_output = os.path.abspath(saveas)
- else:
- self.path_output = os.path.abspath(default)
-
- return self.path_output
-
- # Pseudo-callback to update state when 'status' BitTorrent callback is called.
- def status(self, dict):
- if not self.done:
- if dict.has_key('downRate'):
- self.dl_rate = float(dict['downRate'])
-
- dl_amount = None
- if dict.has_key('downTotal'):
- dl_amount = long(dict['downTotal'] * (1 << 20))
- elif dict.has_key('fractionDone'):
- dl_amount = long(float(dict['fractionDone']) * self.size_total)
- if dl_amount:
- if dl_amount == 0:
- self.dl_pre_amount = self.dl_amount
- self.dl_amount = dl_amount
-
- if dict.has_key('timeEst'):
- self.time_remaining = float(dict['timeEst'])
-
- if dict.has_key('upRate'):
- self.ul_rate = float(dict['upRate'])
-
- if dict.has_key('upTotal'):
- self.ul_amount = long(dict['upTotal'] * (1 << 20))
-
- if dict.has_key('activity'):
- self.activity = dict['activity']
-
- # Incorporate the previous phase(s) in our download amount.
- self.dl_pre_amount += self.dl_amount
- self.dl_amount = 0
-
- # Pseudo-callback to update state when 'finished' BitTorrent callback is called.
- def finished(self):
- self.done = True
- self.dl_amount = self.size_total - self.dl_pre_amount
-
- # Pseudo-callback to update state when 'path' BitTorrent callback is called.
- def path(self, path):
- self.path_output = path
-
- # Pseudo-callback to update state when 'param' BitTorrent callback is called.
- def param(self, params):
- if params:
- self.params = params
-
- if self.params_pounce:
- self.cap_uploads(self.max_uploads)
- self.cap_upload_rate(self.max_upload_rate)
-
- self.params_pounce = False
- else:
- self.params = {}
-
- # Function to run in another thread.
- def download_thread(self, file, status, finished, error, cols, path, param):
- try:
- # BitTorrent 3.3-style
- BitTorrent.download.download(self.args, file, status, finished, error, self.event, cols, path, param)
- except:
- # BitTorrent 3.2-style
- BitTorrent.download.download(self.args, file, status, finished, error, self.event, cols, path)
-
- # Start a session with the specified callbacks (which should each call
- # BtState updaters).
- def download(self, file, status, finished, error, cols, path, param, resuming=False):
- self.done = False
- self.time_begin = time.time()
- self.event = threading.Event()
- self.thread = threading.Thread(None, self.download_thread, 'bt_dl_thread', (file, status, finished, error, cols, path, param))
- self.dl_rate = 0.0
- self.dl_amount = 0
- self.dl_pre_amount = 0
- self.ul_rate = 0.0
- self.params = None
- self.params_pounce = True
-
- if resuming:
- if self.ul_amount:
- self.ul_pre_amount += self.ul_amount
- else:
- self.ul_pre_amount = 0
- self.ul_amount = None
-
- self.thread.start()
-
- # Try to end the BitTorrent session and wait for it to die before
- # returning.
- def join(self):
- if self.event:
- self.event.set()
- self.event = None
- if self.thread:
- self.thread.join()
- self.thread = None
-
- # Cap the number of peers to which you will upload.
- def cap_uploads(self, uploads):
- self.max_uploads = int(uploads)
-
- if self.params and self.params.has_key('max_uploads'):
- self.params['max_uploads'](self.max_uploads)
- return self.max_uploads
- else:
- return None
-
- # Cap the total bytes/sec of which you will upload.
- def cap_upload_rate(self, upload_rate):
- self.max_upload_rate = upload_rate
-
- if self.params and self.params.has_key('max_upload_rate'):
- self.params['max_upload_rate'](int(self.max_upload_rate * (1 << 10)))
- return int(self.max_upload_rate * (1 << 10))
- else:
- return None
-
- # Persistance functions to resume a previously downloaded torrent
- def check_for_previous_save_location(bt_state_args):
- config_dir = get_config_dir()
- cache_dir = os.path.join(config_dir, 'cache')
-
- if path_exists(cache_dir):
- torrent_file = bt_state_args.get_torrent_file()
-
- if torrent_file:
- digest = sha.new(torrent_file).hexdigest()
- digest_url = os.path.join(cache_dir, digest)
-
- if path_exists(digest_url):
- previous_save_location = get_url(digest_url, max_torrent_size)
-
- if path_exists(fix_up_uri(previous_save_location)):
- return previous_save_location
- else:
- print >> sys.stderr, 'check_for_previous_save_location failed to get torrent_file'
- else:
- make_config_dir()
-
- def cache_save_location(bt_state_args):
- config_dir = get_config_dir()
- cache_dir = os.path.join(config_dir, 'cache')
-
- if not path_exists(cache_dir):
- make_config_dir()
-
- # Just to make sure
- config_dir = get_config_dir()
- cache_dir = os.path.join(config_dir, 'cache')
-
- torrent_file = bt_state_args.get_torrent_file()
-
- if torrent_file:
- digest = sha.new(torrent_file).hexdigest()
- digest_url = os.path.join(cache_dir, digest)
-
- digest_file = file(digest_url, 'w')
- digest_file.write(bt_state_args.path_output)
- digest_file.close()
- else:
- print >> sys.stderr, 'cache_save_location failed to get torrent_file'
-
- class GtkClient:
- def __init__(self, args):
- # Miscellaneous events that have happened in this process's
- # BitTorrent sessions.
- self.bt_events = []
-
- # Time that the last error dialog was displayed
- self.last_error = 0
-
- # Localization Setup
- gtk.glade.bindtextdomain(app_name, '/usr/share/locale')
- gtk.glade.textdomain(app_name)
- gettext.bindtextdomain(app_name, '/usr/share/locale')
- gettext.textdomain(app_name)
-
- # Gtk+ Setup
- gtk.threads_init()
-
- # GConf Setup
- self.gconf_client = gconf.client_get_default()
-
- self.gconf_client.add_dir('/apps/'+app_name, gconf.CLIENT_PRELOAD_RECURSIVE)
- #self.gconf_client.notify_add('/apps/'+app_name+'/settings', self.on_gconf_settings_notify)
-
- # Bt Setup
- bt_state_args = BtState.Args(args[1:])
-
- if not bt_state_args.path_origin or not path_exists(bt_state_args.path_origin):
- filters = ((True, _('BitTorrent meta files'), 'mime', 'application/x-bittorrent'), )
- result = GtkFileOpenDialog(_('Open location for BitTorrent meta file'), filters=filters, modal=True).run()
-
- if result:
- bt_state_args.set_path_origin(fix_up_uri(result))
- else:
- # They hit Cancel
- sys.exit(1)
-
- if not bt_state_args.path_output:
- previous_save_location = check_for_previous_save_location(bt_state_args)
-
- if previous_save_location:
- ret = GtkHigContinueSessionDialog(previous_save_location, modal=True).run()
-
- if ret == None:
- # They closed the window without an answer
- sys.exit(2)
- elif ret:
- bt_state_args.set_path_output(previous_save_location)
-
- if not bt_state_args.path_output:
- previous_path = None
- default = None
-
- # Look up the previous save path.
- try:
- previous_path = self.gconf_client.get_string('/apps/'+app_name+'/previous_path')
-
- if not os.path.isdir(previous_path):
- previous_path = None
- except:
- pass
-
- # Run Gtk+ file selector; localonly=True due to
- # BitTorrent.
- result = GtkFileSaveDialog(_('Save location for BitTorrent session'), default_name=bt_state_args.find_suggested_path_output(), default_path=previous_path, modal=True, localonly=True, folder=bt_state_args.test_has_multiple_files()).run()
-
- if result:
- bt_state_args.set_path_output(result)
-
- # Save the new path
- try:
- self.gconf_client.set_string('/apps/'+app_name+'/previous_path', os.path.dirname(result))
- except:
- pass
-
- cache_save_location(bt_state_args)
- else:
- # They hit Cancel
- sys.exit(3)
-
- if not bt_state_args.min_port or not bt_state_args.max_port:
- min_port = self.gconf_client.get_int('/apps/'+app_name+'/settings/min_port')
- max_port = self.gconf_client.get_int('/apps/'+app_name+'/settings/max_port')
-
- if min_port and max_port:
- bt_state_args.set_ports(min_port, max_port)
-
- self.bt_state = BtState(bt_state_args)
-
- # Run Gtk+ main window
- self.glade_xml = gtk.glade.XML(locate_file('data', 'dlsession.glade', 'glade'))
-
- self.setup_treeview_events()
-
- self.glade_xml.signal_autoconnect({
- 'on_window_main_destroy':
- self.on_window_main_destroy,
- 'on_button_open_clicked':
- self.on_button_open_clicked,
- 'on_button_resume_clicked':
- self.on_button_resume_clicked,
- 'on_button_stop_clicked':
- self.on_button_stop_clicked,
- 'on_button_close_clicked':
- self.on_button_close_clicked,
- 'on_checkbutton_cap_uploads_toggled':
- self.on_checkbutton_cap_uploads_toggled,
- 'on_spinbutton_cap_uploads_value_changed':
- self.on_spinbutton_cap_uploads_value_changed,
- 'on_checkbutton_cap_upload_rate_toggled':
- self.on_checkbutton_cap_upload_rate_toggled,
- 'on_spinbutton_cap_upload_rate_value_changed':
- self.on_spinbutton_cap_upload_rate_value_changed,
- 'on_button_events_clear_clicked':
- self.on_button_events_clear_clicked,
- 'on_checkbutton_events_display_error_dialogs_toggled':
- self.on_checkbutton_events_display_error_dialogs_toggled
- })
-
- self.glade_xml.get_widget('label_download_address_output').set_text(gnomevfs.format_uri_for_display(str(self.bt_state.path_origin)))
- self.glade_xml.get_widget('window_main').set_title(os.path.basename(self.bt_state.path_output))
-
- # Set GUI preferences
- display_error_dialogs = self.gconf_client.get_bool('/apps/'+app_name+'/settings/display_error_dialogs')
- if display_error_dialogs != None:
- self.glade_xml.get_widget('checkbutton_events_display_error_dialogs').set_active(display_error_dialogs)
-
- if bt_state_args.max_uploads:
- self.glade_xml.get_widget('spinbutton_cap_uploads').set_value(bt_state_args.max_uploads)
- self.glade_xml.get_widget('checkbutton_cap_uploads').set_active(True)
- else:
- cap_upload_peers = self.gconf_client.get_int('/apps/'+app_name+'/settings/cap_upload_peers')
- if cap_upload_peers != None:
- self.glade_xml.get_widget('spinbutton_cap_uploads').set_value(cap_upload_peers)
-
- cap_upload = self.gconf_client.get_bool('/apps/'+app_name+'/settings/cap_upload')
- if cap_upload != None:
- self.glade_xml.get_widget('checkbutton_cap_uploads').set_active(cap_upload)
-
- cap_upload_rate_kbps = self.gconf_client.get_float('/apps/'+app_name+'/settings/cap_upload_rate_kbps')
- if cap_upload_rate_kbps != None:
- self.glade_xml.get_widget('spinbutton_cap_upload_rate').set_value(cap_upload_rate_kbps)
-
- if bt_state_args.max_upload_rate != None:
- if bt_state_args.max_upload_rate > 0:
- self.glade_xml.get_widget('spinbutton_cap_upload_rate').set_value(bt_state_args.max_upload_rate)
- self.glade_xml.get_widget('checkbutton_cap_upload_rate').set_active(True)
- else:
- self.glade_xml.get_widget('checkbutton_cap_upload_rate').set_active(False)
- else:
- cap_upload_rate = self.gconf_client.get_bool('/apps/'+app_name+'/settings/cap_upload_rate')
- if cap_upload_rate != None:
- self.glade_xml.get_widget('checkbutton_cap_upload_rate').set_active(cap_upload_rate)
-
- # Save arguments for session recovery
- self.session_recovery_cwd = os.getcwd()
- self.session_recovery_args = args[:1] + bt_state_args.get_args()
-
- # Setup session
- master_client = gnome.ui.master_client()
-
- master_client.connect('save-yourself', self.on_gc_save_yourself, None)
- master_client.connect('die', self.on_gc_die, None)
-
- master_client.set_restart_style(gnome.ui.RESTART_IF_RUNNING)
-
- # Run Bt
- self.run_bt()
-
- # Run Gtk+
- gtk.main()
-
- # Appends an event to the log.
- def log_event(self, type, text):
- t = fmt_time_short(time.time() - self.bt_state.time_begin)
-
- notebook_main = self.glade_xml.get_widget('notebook_main')
- vbox_events = self.glade_xml.get_widget('vbox_events')
- events_tab = notebook_main.page_num(vbox_events)
-
- # Check if the user is looking at the events tab. If so, do not
- # pop up an error dialog.
- if events_tab != None and notebook_main.get_current_page() != events_tab:
- if type == 'Error':
- # Select the events tab now (this will also prevent
- # more error dialogs from popping up).
- #
- # Say it with me: "worst solution EVER". :P
- notebook_main.set_current_page(events_tab)
-
- if self.glade_xml.get_widget('checkbutton_events_display_error_dialogs').get_active():
- # Try to specially adapt the error message.
- try:
- mo = re.search(r"([A-Za-z ',]+) - [<]?([^>]+)[>]?", str(text))
-
- GtkHigErrorDialog(mo.group(1), mo.group(2)).run()
- except:
- GtkHigErrorDialog(str(text)).run()
-
- if self.bt_events:
- treeview_events = self.glade_xml.get_widget('treeview_events')
-
- # Scroll to the newly added event
- iter = self.bt_events.append((t, type, text))
- path = treeview_events.get_model().get_path(iter)
- treeview_events.scroll_to_cell(path)
- else:
- print >> sys.stderr, i18n('%s, %s: %s' % (t, type, text))
-
- # BitTorrent callbacks
- def on_bt_file(self, default, size, saveas, dir):
- path = self.bt_state.file(default, size, saveas, dir)
-
- gtk.threads_enter()
-
- label_download_file_output = self.glade_xml.get_widget('label_download_file_output')
- label_download_file_output.set_text(path)
-
- gtk.threads_leave()
-
- return path
-
- def on_bt_status(self, dict = {}, fractionDone = None, timeEst = None, downRate = None, upRate = None, activity = None):
- # To support BitTorrent 3.2, pack anything supplied seperately
- # from dict into dict.
- if fractionDone:
- dict['fractionDone'] = fractionDone
- if timeEst:
- dict['timeEst'] = timeEst
- if downRate:
- dict['downRate'] = downRate
- if upRate:
- dict['upRate'] = upRate
- if activity:
- dict['activity'] = activity
-
- self.bt_state.status(dict)
-
- gtk.threads_enter()
-
- label_download_elapsed_output = self.glade_xml.get_widget('label_download_time_elapsed_output')
- label_download_elapsed_output.set_text(fmt_time_long_precise_verbose(time.time() - self.bt_state.time_begin))
-
- if dict.has_key('fractionDone'):
- progressbar_download_status = self.glade_xml.get_widget('progressbar_download_status')
- window_main = self.glade_xml.get_widget('window_main')
-
- perc_string = str(int(dict['fractionDone'] * 100)) + '%'
-
- progressbar_download_status.set_fraction(dict['fractionDone'])
- progressbar_download_status.set_text(perc_string)
- window_main.set_title(perc_string + ' of ' + os.path.basename(self.bt_state.path_output))
-
- if dict.has_key('downTotal') or dict.has_key('fractionDone'):
- label_download_status_output = self.glade_xml.get_widget('label_download_status_output')
-
- label_download_status_output.set_text('%.1f of %.1f MB at %.2f KB/s' %
- (float(self.bt_state.get_dl_amount()) / (1 << 20),
- float(self.bt_state.size_total) / (1 << 20),
- float(self.bt_state.dl_rate) / (1 << 10)))
-
- if dict.has_key('timeEst'):
- label_download_time_remaining_output = self.glade_xml.get_widget('label_download_time_remaining_output')
-
- label_download_time_remaining_output.set_text(fmt_time_long(dict['timeEst']))
-
- if dict.has_key('upRate') or dict.has_key('upTotal'):
- label_upload_status_output = self.glade_xml.get_widget('label_upload_status_output')
-
- if self.bt_state.get_ul_amount():
- label_upload_status_output.set_text(_('%.1f MB at %.2f KB/s') %
- (float(self.bt_state.get_ul_amount()) / (1 << 20),
- float(self.bt_state.ul_rate) / (1 << 10)))
- else:
- label_upload_status_output.set_text('%.2f KB/s' %
- (float(self.bt_state.ul_rate) / (1 << 10)))
-
- if dict.has_key('activity'):
- self.log_event('Activity', dict['activity'])
-
- gtk.threads_leave()
-
- def on_bt_finished(self):
- self.bt_state.finished()
- self.on_bt_status({'fractionDone': float(1.0), 'timeEst': 0, 'activity': 'finished'})
-
- gtk.threads_enter()
-
- progressbar_download_status = self.glade_xml.get_widget('progressbar_download_status')
- label_download_time_remaining_output = self.glade_xml.get_widget('label_download_time_remaining_output')
- button_open = self.glade_xml.get_widget('button_open')
-
- progressbar_download_status.set_fraction(1.0)
- progressbar_download_status.set_text('100%')
- label_download_time_remaining_output.set_text(_('None'))
-
- # Check if the completed session can be 'shown'
- if can_show_path and show_path and can_show_path(self.bt_state.path_output):
- button_open.set_sensitive(True)
-
- gtk.threads_leave()
-
- def on_bt_error(self, msg):
- gtk.threads_enter()
-
- self.log_event('Error', msg)
-
- gtk.threads_leave()
-
- def on_bt_path(self, path):
- self.bt_state.path(path)
-
- gtk.threads_enter()
-
- label_download_file_output = self.glade_xml.get_widget('label_download_file_output')
- label_download_file_output.set_text(self.bt_state.path_output)
-
- gtk.threads_leave()
-
- def on_bt_param(self, params):
- self.bt_state.param(params)
-
- gtk.threads_enter()
-
- checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
- spinbutton_cap_uploads = self.glade_xml.get_widget('spinbutton_cap_uploads')
- checkbutton_cap_upload_rate = self.glade_xml.get_widget('checkbutton_cap_upload_rate')
- spinbutton_cap_upload_rate = self.glade_xml.get_widget('spinbutton_cap_upload_rate')
-
- if params.has_key('max_uploads'):
- checkbutton_cap_uploads.set_sensitive(True)
- spinbutton_cap_uploads.set_sensitive(True)
- else:
- checkbutton_cap_uploads.set_sensitive(False)
- spinbutton_cap_uploads.set_sensitive(False)
-
- if params.has_key('max_upload_rate'):
- checkbutton_cap_upload_rate.set_sensitive(True)
- spinbutton_cap_upload_rate.set_sensitive(True)
- else:
- checkbutton_cap_upload_rate.set_sensitive(False)
- spinbutton_cap_upload_rate.set_sensitive(False)
-
- gtk.threads_leave()
-
- # GnomeClient callbacks
- def on_gc_save_yourself(self, client, phase, save_style, is_shutdown, interact_style, is_fast, data=None):
- master_client = gnome.ui.master_client()
-
- args = self.session_recovery_args[:]
-
- checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
- spinbutton_cap_uploads = self.glade_xml.get_widget('spinbutton_cap_uploads')
- checkbutton_cap_upload_rate = self.glade_xml.get_widget('checkbutton_cap_upload_rate')
- spinbutton_cap_upload_rate = self.glade_xml.get_widget('spinbutton_cap_upload_rate')
-
- if checkbutton_cap_uploads.get_active():
- args.append('--max_uploads')
- args.append(str(spinbutton_cap_uploads.get_value()))
-
- if checkbutton_cap_upload_rate.get_active():
- args.append('--max_upload_rate')
- args.append(str(spinbutton_cap_upload_rate.get_value()))
-
- master_client.set_current_directory(self.session_recovery_cwd)
- master_client.set_clone_command(len(args), args)
- master_client.set_restart_command(len(args), args)
-
- return True
-
- def on_gc_die(self, client, data=None):
- self.join()
- gtk.main_quit()
-
- # GTK+ callbacks
- def on_window_main_destroy(self, widget, data=None):
- self.join()
- gtk.main_quit()
-
- def on_button_open_clicked(self, widget, data=None):
- if show_path:
- show_path(self.bt_state.path_output)
-
- def on_button_resume_clicked(self, widget, data=None):
- button_resume = self.glade_xml.get_widget('button_resume')
- button_stop = self.glade_xml.get_widget('button_stop')
- button_close = self.glade_xml.get_widget('button_close')
-
- button_resume.set_sensitive(False)
- button_stop.show()
- button_close.hide()
-
- self.run_bt(resuming=True)
-
- def on_button_stop_clicked(self, widget, data=None):
- self.join()
-
- button_resume = self.glade_xml.get_widget('button_resume')
- button_stop = self.glade_xml.get_widget('button_stop')
- button_close = self.glade_xml.get_widget('button_close')
-
- button_resume.set_sensitive(True)
- button_stop.hide()
- button_close.show()
-
- def on_button_close_clicked(self, widget, data=None):
- window_main = self.glade_xml.get_widget('window_main')
- window_main.destroy()
-
- def on_checkbutton_cap_uploads_toggled(self, widget, data=None):
- spinbutton_cap_uploads = self.glade_xml.get_widget('spinbutton_cap_uploads')
-
- try:
- if widget.get_active():
- self.bt_state.cap_uploads(int(spinbutton_cap_uploads.get_value()))
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/cap_upload', True)
- else:
- self.bt_state.cap_uploads(0)
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/cap_upload', False)
- except:
- pass
-
- def on_spinbutton_cap_uploads_value_changed(self, widget, data=None):
- checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
-
- try:
- self.gconf_client.set_int('/apps/'+app_name+'/settings/cap_upload_peers', int(widget.get_value()))
- except:
- pass
-
- if checkbutton_cap_uploads.get_active():
- self.bt_state.cap_uploads(int(widget.get_value()))
-
- def on_checkbutton_cap_upload_rate_toggled(self, widget, data=None):
- spinbutton_cap_upload_rate = self.glade_xml.get_widget('spinbutton_cap_upload_rate')
-
- try:
- if widget.get_active():
- self.bt_state.cap_upload_rate(spinbutton_cap_upload_rate.get_value())
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/cap_upload_rate', True)
- else:
- self.bt_state.cap_upload_rate(0)
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/cap_upload_rate', False)
- except:
- pass
-
- def on_spinbutton_cap_upload_rate_value_changed(self, widget, data=None):
- checkbutton_cap_upload_rate = self.glade_xml.get_widget('checkbutton_cap_upload_rate')
-
- try:
- self.gconf_client.set_float('/apps/'+app_name+'/settings/cap_upload_rate_kbps', float(widget.get_value()))
- except:
- pass
-
- if checkbutton_cap_upload_rate.get_active():
- self.bt_state.cap_upload_rate(widget.get_value())
-
- def on_button_events_clear_clicked(self, widget, data=None):
- if self.bt_events:
- self.bt_events.clear()
-
- def on_checkbutton_events_display_error_dialogs_toggled(self, widget, data=None):
- try:
- if widget.get_active():
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/display_error_dialogs', True)
- else:
- self.gconf_client.set_bool('/apps/'+app_name+'/settings/display_error_dialogs', False)
- except:
- pass
-
- # GTK+ setup stuff to supliment Glade.
- def setup_treeview_events(self):
- treeview_events = self.glade_xml.get_widget('treeview_events')
-
- list_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)
-
- treeview_events.set_model(list_store)
-
- treeview_events.append_column(gtk.TreeViewColumn('Time', gtk.CellRendererText(), text=0))
- treeview_events.append_column(gtk.TreeViewColumn('Type', gtk.CellRendererText(), text=1))
- treeview_events.append_column(gtk.TreeViewColumn('Text', gtk.CellRendererText(), text=2))
-
- self.bt_events = list_store
-
- # Helpful wrapper to start BitTorrent session.
- def run_bt(self, resuming=False):
- self.bt_state.download(self.on_bt_file, self.on_bt_status, self.on_bt_finished, self.on_bt_error, 100, self.on_bt_path, self.on_bt_param, resuming=resuming)
-
- # Helpful wrapper to end BitTorrent session.
- def join(self):
- if self.bt_state:
- self.bt_state.join()
-
- # Start the client
- def run(args):
- client = GtkClient(args)
-
- # Automatically start the client as long as this isn't being used as a module
- # for some reason.
- if __name__ == '__main__':
- gtk.window_set_default_icon_from_file('/usr/share/gnome-btdownload/pixmaps/download.png')
- run(sys.argv)
-