#!/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)
