#!/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.vfs

# The name of this program.
app_name         = 'gnome-btdownload'

# The version of this program.
app_version      = '0.0.18'

# 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:
		uri = gnome.vfs.URI(path)
		return str(uri)
	except:
		return None

# Checks if a path exists. This is still around from when this was a 'virtual'
# function.
def path_exists(path):
	return gnome.vfs.exists(path)

# GNOME wrapper
def can_show_path(path):
	return True
def show_path(path):
	gnome.url_show(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):
		gnome.vfs.make_directory(config_dir, 00750)
	
	cache_dir = os.path.join(config_dir, 'cache')
	
	if not path_exists(cache_dir):
		gnome.vfs.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):
	handle = gnome.vfs.open(uri, gnome.vfs.OPEN_READ)
	
	return handle.read(read_bytes)

# 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 more verbose displayable string.
def fmt_time_long(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)
	))

# 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')
		
		self.glade_xml.get_widget('label_text').set_markup(text)
		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
		else:
			return False
	
	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=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 and (action == gtk.FILE_CHOOSER_ACTION_SAVE or action == gtk.FILE_CHOOSER_ACTION_CREATE_FOLDER):
			self.dialog.set_current_name(default)
		
		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, modal, multiple, localonly)

# A wrapper for the save file dialog.
class GtkFileSaveDialog(GtkFileActionDialog):
	def __init__(self, title, folder=False, filters=None, default=None, modal=False, multiple=False, localonly=False):
		action = gtk.FILE_CHOOSER_ACTION_SAVE
		
		if folder:
			# Is it just me, or does FILE_CHOOSER_ACTION_CREATE_FOLDER not do anything?
			action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
		
		GtkFileActionDialog.__init__(self, title, action, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT), filters, default, 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):
			self.args                  = []
			self.path_origin           = None
			self.path_output           = None
			self.suggested_path_output = 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(args[i+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:
						uri = fix_up_uri(args[i])
						
						if uri:
							self.path_origin = uri
							
							self.args.append('--url')
							self.args.append(self.path_origin)
			
			# If we couldn't deduce a suggested_path_output, base
			# one upon the name of the meta file.
			if not self.suggested_path_output and self.path_origin:
				try:
					mo = re.search(rfc_2396_uri_regexp, self.path_origin)
					
					self.suggested_path_output = os.path.basename(mo.group(5))
					
					if self.suggested_path_output[-len('.torrent'):] == '.torrent':
						self.suggested_path_output = self.suggested_path_output[:-len('.torrent')]
				except:
					pass
		
		# If we don't already have a path_output, try to find a
		# suggested_path_output from the data in the meta file.
		def find_suggested_path_output(self):
			self.suggested_path_output = None
			
			if self.path_origin and not self.path_output:
				try:
					torrent_file = get_url(self.path_origin, max_torrent_size)
					torrent_info = BitTorrent.bencode.bdecode(torrent_file)
					
					self.suggested_path_output = torrent_info['info']['name']
				except:
					pass
		
		# Set the path_origin and update anything that might depend
		# upon it if it's not already set.
		def set_path_origin(self, path_origin):
			if not self.path_origin:
				self.path_origin = path_origin
				self.args.append('--url')
				self.args.append(fix_up_uri(path_origin))
				
				self.find_suggested_path_output()
			else:
				# Fail silently
				pass
		
		# Set the path_output if it's not already set.
		def set_path_output(self, path_output):
			if not self.path_output:
				self.path_output = path_output
				self.args.append('--saveas')
				self.args.append(path_output)
			else:
				# Fail silently
				pass
	
	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.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):
		digest     = sha.new(get_url(bt_state_args.path_origin, max_torrent_size)).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(previous_save_location):
				return previous_save_location
	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')
	
	digest     = sha.new(get_url(bt_state_args.path_origin, max_torrent_size)).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()

class GtkClient:
	def __init__(self, args):
		# Miscellaneous events that have happened in this process's
		# BitTorrent sessions.
		self.bt_events = []
		
		# Gtk+ Setup
		gtk.threads_init()
		
		# Bt Setup
		bt_state_args = BtState.Args(args)
		
		if not 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(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:
				if GtkHigContinueSessionDialog(previous_save_location, modal=True).run():
					bt_state_args.set_path_output(previous_save_location)
		
		if not bt_state_args.path_output:
			# Run Gtk+ file selector; localonly=True due to
			# BitTorrent.
			result = GtkFileSaveDialog('Save location for BitTorrent session', default=bt_state_args.suggested_path_output, modal=True, localonly=True).run()
			
			if result:
				bt_state_args.set_path_output(result)
				
				cache_save_location(bt_state_args)
			else:
				# They hit Cancel
				sys.exit(2)
		
		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
		})
		
		self.glade_xml.get_widget('label_download_address_output').set_text(self.bt_state.path_origin)
		self.glade_xml.get_widget('window_main').set_title(self.bt_state.path_origin)
		
		# 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)
		
		if type == 'Error' and 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('<b>' + mo.group(1) + '</b>', mo.group(2))
			except:
				GtkHigErrorDialog(str(text))
		
		if self.bt_events:
			self.bt_events.append((t, type, text))
		else:
			print >> sys.stderr, '%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(time.time() - self.bt_state.time_begin))
		
		if dict.has_key('spew'):
			print >> sys.stderr, 'Spew: %s' % (dict['spew'])
		
		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 ' + self.bt_state.path_origin)
		
		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(fmt_time_long(0))
		
		# 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()
	
	# 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')
		
		if widget.get_active():
			self.bt_state.cap_uploads(int(spinbutton_cap_uploads.get_value()))
		else:
			self.bt_state.cap_uploads(0)
	
	def on_spinbutton_cap_uploads_value_changed(self, widget, data=None):
		checkbutton_cap_uploads = self.glade_xml.get_widget('checkbutton_cap_uploads')
		
		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')
		
		if widget.get_active():
			self.bt_state.cap_upload_rate(spinbutton_cap_upload_rate.get_value())
		else:
			self.bt_state.cap_upload_rate(0)
	
	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')
		
		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()
	
	# 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 with non-program command line arguments.
def run(args):
	client = GtkClient(args)

# Automatically start the client if this isn't being used as a module for some
# reason.
if __name__ == '__main__':
	run(sys.argv[1:])
