#!/usr/bin/python

# This is a part of the external applets for Cairo-Dock
# Copyright : (C) 2011 by Fabounet
# E-mail : fabounet@glx-dock.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# http://www.gnu.org/licenses/licenses.html#GPL
#
# The RTC part is adapted from 'transmissionrpc' (Erik Svensson <erik.public@gmail.com>)

####################
### dependancies ###
####################
from __future__ import print_function
try:
	import json
except ImportError:
	import simplejson as json
from os import popen
import subprocess
import os.path

try:
	from gobject import timeout_add
except:
	from gi.repository.GObject import timeout_add

from CDApplet import CDApplet, _

try: # python3
	import http.client as httplib, urllib.request, urllib.error
	urllib_request = urllib.request
	urllib_error = urllib.error
except: # python2
	import httplib, urllib2
	urllib_request = urllib2
	urllib_error = urllib2

TR_STATUS_CHECK_WAIT   = (1<<0)
TR_STATUS_CHECK        = (1<<1)
TR_STATUS_DOWNLOAD     = (1<<2)
TR_STATUS_SEED         = (1<<3)
TR_STATUS_STOPPED      = (1<<4)

# Quick-info enum
INFO_NONE           = 0
INFO_DL_SPEED       = 1
INFO_PERCENT        = 2
INFO_PERCENT_NUMBER = 3
INFO_REMAINING_TIME = 4

def format_bytes(x):
	unit=''
	if x < 1024:
		unit = 'B'
		y = x
	elif x < 1048576:
		unit = 'K'
		y = float(x) / 1024
	elif x < 1073741824:
		unit = 'M'
		y = float(x) / 1048576
	else:
		unit = 'G'
		y = float(x) / 1073741824
	return y,unit

html_escape_table = {
	"&": "&amp;",
	'"': "&quot;",
	"'": "&apos;",
	">": "&gt;",
	"<": "&lt;",
	}

def html_escape(text):
	"""Produce entities within text."""
	return "".join(html_escape_table.get(c,c) for c in text)

####################
### Applet class ###
####################
class Applet(CDApplet):
	def __init__(self):
		# define internal variables
		self.cClass = ''
		self.bHasFocus = False
		self.iSidGetData = 0
		self.iSidTryConnect = 0
		self.iSessionID = 0
		self.url = ''
		
		# call high-level init
		CDApplet.__init__(self)
	
	##### private methods #####
	
	# Try to make a connection with no arguments, so we get the 409 error code.
	# The headers will contain our session ID.
	def get_session_id(self):
		query = json.dumps({'method': 'session-get', 'arguments': {}})
		request = urllib_request.Request(self.url, query.encode('UTF-8'), {})
		try:
			response = urllib_request.urlopen(request, timeout=30)
		except urllib_error.HTTPError as error:
			#~ aerror_data = error.read()
			if error.code == 409:
				self.iSessionID = error.headers['X-Transmission-Session-Id']
				print("*** iSessionID:",self.iSessionID)
		except :
			return

	def get_time_from_eta(self, t, bWithText):
		info = ""
		d=h=m=s=0
		if t > 86400:
			if bWithText: # 2 days 12h
				d = int(t / 86400) # at least 1 day
				h = int((t - d*86400) / 3600)
				info += str(d) + " " + (_("days") if d > 1 else _("day")) + " "
				if h > 0:
					info += str(h) + _("h")
			else: # 60h
				h = int(d*86400 / 3600)
				info += str(h) + _("h")
		else:
			# with text: 2h 12min 5sec
			# without  : 2:12:05
			h = int(t / 3600)
			m = int((t - h*3600) / 60)
			s = int(t - h*3600 - m*60)
			if h > 0:
				info += str(h)
				if bWithText:
					info += _("h") + " "
				else:
					info += ":"
			if h > 0 or m > 0:
				if bWithText:
					info += str(m) + _("min") + " "
				else:
					info += format(m, "02") + ":"
			if bWithText:
				info += str(s) + _("sec")
			else:
				info += format(s, "02")

		return info

	def update_no_data(self):
		print("no data from Transmission, it may have shut down.")
		if self.iSidTryConnect == 0: # start trying to connect.
			self.iSidTryConnect = timeout_add(2000,self.try_connect)
		self.iSidGetData = 0  # and stop listening for sessions's data
		self.icon.AddDataRenderer('', 0, '')
		self.icon.SetQuickInfo('')
		return False

	def update_data(self):
		# if connection closed, start trying
		if self.iSessionID == 0: # if connection closed
			if self.iSidTryConnect == 0: # start trying to connect.
				self.iSidTryConnect = timeout_add(2000,self.try_connect)
			self.iSidGetData = 0  # and stop listening for sessions's data
			self.icon.AddDataRenderer('', 0, '')
			self.icon.SetQuickInfo('')
			return False
		
		if self.config['quick-info'] == INFO_DL_SPEED:  # display download speed
			# get sessions's data
			data = self._request ("session-stats", {} )  # Request arguments: none
			
			# if no result, start trying to reconnect.
			if data == None or data['result'] != 'success':  # no data from transmission
				return self.update_no_data()
			
			# display the new data
			rate = data['arguments']['downloadSpeed']
			unit=''
			if rate < 100:
				rate = 0
			rate,unit = format_bytes(rate)
			
			form = ''
			if rate == 0:
				form = ".0f"
			elif rate < 10:
				form = ".1f"
			else:
				form = ".0f"
				
			self.icon.SetQuickInfo(format(rate,form)+unit)

		elif self.config['quick-info'] == INFO_REMAINING_TIME:  # remaining time of the slowest active torrent
			data = self._request ("torrent-get", {"fields": ["eta", "status"]})
			if data == None or data['result'] != 'success':
				return self.update_no_data()

			maxTime = 0
			for value in data['arguments']['torrents']:
				if value['status'] == TR_STATUS_DOWNLOAD and value['eta'] > maxTime:  # active torrent and the slowest
					maxTime = value['eta']
			if maxTime > 0:
				self.icon.SetQuickInfo(self.get_time_from_eta(maxTime, False))
			else:
				self.icon.SetQuickInfo('')

		if self.config['quick-info'] == INFO_PERCENT or self.config['quick-info'] == INFO_PERCENT_NUMBER or self.config['progressbar']:  # display percent or percent+number of active torrents or progress as a progressbar
			# get torrents data
			data = self._request ("torrent-get", { "ids": "recently-active",
				"fields": [ "percentDone" ] } )
			
			# if no result, start trying to reconnect.
			if data == None or data['result'] != 'success':  # no data from transmission
				return self.update_no_data()
				
			percent = 0.
			n = 0
			for value in data['arguments']['torrents']:
				percent = percent + value['percentDone']
				n = n + 1
			
			if n != 0:
				percent = percent / n
				if self.config['progressbar']:
					self.icon.RenderValues([percent])
				if self.config['quick-info'] == INFO_PERCENT:
					self.icon.SetQuickInfo(format(percent*100,".0f")+'%')
				elif self.config['quick-info'] == INFO_PERCENT_NUMBER:
					self.icon.SetQuickInfo(format(percent*100,".0f")+'% ('+format(n,"d")+')')
			else:  # no active torrent
				if self.config['progressbar']:
					self.icon.RenderValues([0])
				if self.config['quick-info'] == INFO_PERCENT or self.config['quick-info'] == INFO_PERCENT_NUMBER:
					self.icon.SetQuickInfo('')
		
		return True
		
	def try_connect(self):
		# try to grab the session
		if self.iSessionID == 0:
			self.get_session_id()
		
		# start listening for session's data or keep trying to connect.
		if self.iSessionID != 0:  # if session is ok
			if self.iSidGetData == 0:  # start listening for session's data
				self.iSidGetData = timeout_add(2000,self.update_data)
			self.iSidTryConnect = 0  # and stop connecting
			if self.config['progressbar']:  # add a progress bar if required
				self.icon.AddDataRenderer('progressbar', 1, '')
			return False
		else:  # else, keep trying to connect
			return True
	
	def _http_query(self, query):
		headers = {'x-transmission-session-id': self.iSessionID}
		try:
			request = urllib_request.Request(self.url, query.encode('UTF-8'), headers)
			response = urllib_request.urlopen(request, timeout=20)
			return response.read()
		except urllib_error.HTTPError as error:
			print("HTTP error:",error.filename, error.code, error.msg)
		except urllib_error.URLError as error:
			print("URL error:",error.reason)
		except httplib.BadStatusLine as error:
			print("bad status error:",error.line)
		except:
			print("query error")
			return None
	
	def _request(self, method, arguments={}):
		query = json.dumps({'method': method, 'arguments': arguments})
		http_data = self._http_query(query)
		try:
			return json.loads(http_data.decode('UTF-8'))
		except :
			#~ print('Error: ' + str(e))
			self.iSessionID = 0
			if self.iSidTryConnect == 0:
				self.iSidTryConnect = timeout_add(2000,self.try_connect)
			return None
	
	def show_torrents_info(self):
		if self.iSessionID == 0:
			self.icon.ShowDialog(_("Transmission is not running, or is not responding to us."), 4)
			return
		
		# get torrents data
		data = self._request ("torrent-get", { "fields": [
			"name", "percentDone", "eta", "status", "uploadRatio", "peersConnected", "peersSendingToUs", "webseedsSendingToUs" ] } )
		if data == None or data['result'] != 'success':
			self.icon.ShowDialog(_("Couldn't get data from Transmission. You need Transmission 1.9 or above."), 4)
			return
		info = ""
		n = 0
		for value in data['arguments']['torrents']:
			info += "<b>"+html_escape(value['name'])+"</b>:\n"
			info += "  " + _("Progress:") + " <b>"+format(value['percentDone']*100,".1f")+'%</b>'
			state = value['status']
			if state == TR_STATUS_STOPPED:
				info += " <i>(" + _("paused") + ")</i>\n"
			elif state == TR_STATUS_CHECK:
				info += " <i>(" + _("checking") + ")</i>\n"
			elif state == TR_STATUS_CHECK_WAIT:
				info += " <i>(" + _("waiting for check") + ")</i>\n"
			elif state == TR_STATUS_SEED:
				info += " <i>(" + _("seeding") + ")</i>\n"
			elif state == TR_STATUS_DOWNLOAD:  # active torrent
				t = value['eta']
				if t > 0:
					info += " <i>(" + _("Time remaining:") + " "
					info += self.get_time_from_eta(t, True)
					info += ")</i>\n"
				elif t < 0:
					info += " <i>(" + _("Time remaining: Unknown") + ")</i>\n"
				else:
					info += " <i>(" + _("Finished") + ")</i>\n"
				info += "  " + _("Number of peers:") + " " + str(value['peersSendingToUs']) + ", " + _("and seeds:") + " " + str(value['webseedsSendingToUs'])+"\n"
			else:
				info += "\n"

			if state != TR_STATUS_DOWNLOAD and value['peersConnected'] > 0:
				info += "  " + _("Connected peers:") + " " + str(value['peersConnected']) + "\n"
			ratio = value['uploadRatio']
			if ratio >= 0:
				info += "  " + _("Ratio:") + " "+format(ratio,".2f")+'\n'
			n += 1
		
		if n == 0:
			info += "<i>" + _("No torrent in the list") + "</i>\n"
		
		# get session data
		data = self._request ("session-stats", {} )  # Request arguments: none
		if data != None and data['result'] == 'success':
			info += "\n<b>" + _("Total amount of data:") + "</b>\n"
			stats = data['arguments']['current-stats']
			
			dl = stats['downloadedBytes']
			if dl < 100:
				form = ".0f"
			else:
				form = ".1f"
			dl,unit = format_bytes(dl)
			info += " - " + _("Received:") + " "+format(dl,form)+unit+"\n"
			
			ul = stats['uploadedBytes']
			if ul < 100:
				form = ".0f"
			else:
				form = ".1f"
			ul,unit = format_bytes(ul)
			info += " - " + _("Sent:") + " "+format(ul,form)+unit
		
		# pop up dialog
		dialog_attributes = {
			"icon" : "transmission",
			"message" : info,
			"use-markup" : True,
			"time-length" : 4+len(info)/40 }
		widget_attributes = {}
		self.icon.PopupDialog (dialog_attributes, widget_attributes)
		
	def pause_all_torrents(self):
		self._request('torrent-stop', {})  # 'All torrents are used if the "ids" argument is omitted.'
	
	def resume_all_torrents(self):
		self._request('torrent-start', {})  # same as above
	
	def close_session(self):
		self._request('session-close', {})
	
	##### applet definition #####
	
	def get_config(self,keyfile):
		def _get_user_config(self):
			self.config['rpc-enabled'] 	= true
			self.config['server'] 		= keyfile.get('Configuration', 'server')
			self.config['port'] 		= keyfile.getint('Configuration', 'port')
			self.config['user'] 		= keyfile.get('Configuration', 'user')
			self.config['password'] 	= keyfile.get('Configuration', 'password')
		
		self.config['shortkey'] 	= keyfile.get('Configuration', 'shortkey')
		self.config['quick-info'] 	= keyfile.getint('Configuration', 'quick-info')
		self.config['progressbar'] 	= keyfile.getboolean('Configuration', 'progressbar')
		
		bLocal = keyfile.getboolean('Configuration', 'local')
		if bLocal:
			try:
				settings = json.loads(open(os.path.expanduser("~/.config/transmission/settings.json")).read())
			except: # e.g.: No such file
				settings = None
			if settings != None:
				self.config['rpc-enabled'] 	= settings['rpc-enabled']
				self.config['port'] 		= settings['rpc-port']
				self.config['server'] 		= '127.0.0.1'
				if settings['rpc-authentication-required']:
					self.config['user'] 	= settings['rpc-username']
					self.config['password'] = settings['rpc-password']
				else:
					self.config['user'] 	= ''
					self.config['password'] = ''
			else:
				print("Transmission: couldn't read settings")
				self._get_user_config()
		else:
			self._get_user_config()
		
		if self.config['server'] == '':
			self.config['server'] = '127.0.0.1'
		if self.config['port'] == 0:
			self.config['port'] = 9091
		
	def end(self):
		print("*** end of transmission applet")
	
	def begin(self):
		# control the appli
		process = os.popen("pgrep transmission-gtk").read().rstrip()
		if process != '':
			print("transmission-gtk is already running")
			self.cClass = 'transmission-gtk'
		else:
			process = os.popen("pgrep transmission-qt").read().rstrip()
			if process != '':
				print("transmission-qt is already running")
				self.cClass = 'transmission-qt'
			else:
				path = os.popen("which transmission-gtk").read().rstrip()
				print(">>> path to transmission-gtk : "+path)
				if path != '':
					self.cClass = 'transmission-gtk'
				else:
					path = os.popen("which transmission-qt").read().rstrip()
					print(">>> path to transmission-qt : "+path)
					if path != '':
						self.cClass = 'transmission-qt'
					else:
						self.cClass = 'transmission'
		print(">>> class of the appli : "+self.cClass)
		self.icon.ControlAppli(self.cClass)
		
		# get the URL to contact transmission.
		if not self.config['rpc-enabled']:
			self.icon.ShowDialog(_("You have to enable the Web client in the Transmisison's settings."),8)
		
		base_url = 'http://' + self.config['server'] + ':' + str(self.config['port'])
		self.url = base_url + '/transmission/rpc'
		
		# set authentication
		if self.config['user'] and self.config['password']:
			password_manager = urllib_request.HTTPPasswordMgrWithDefaultRealm()
			password_manager.add_password(realm=None, uri=self.url, user=self.config['user'], passwd=self.config['password'])
			opener = urllib_request.build_opener(
				urllib_request.HTTPBasicAuthHandler(password_manager),
				urllib_request.HTTPDigestAuthHandler(password_manager) )
			urllib_request.install_opener(opener)
		
		# start the connection timer.
		if self.iSidTryConnect == 0:
			self.iSidTryConnect = timeout_add(2000,self.try_connect)
		
		# bind the shortkey to the torrents info dialog
		self.icon.BindShortkey([self.config['shortkey']])
		
	
	def reload(self):
		# re-bind the shortkey
		self.icon.BindShortkey([self.config['shortkey']])
		# set or remove the progress bar
		if self.config['progressbar']:
			self.icon.AddDataRenderer('progressbar', 1, '')
		else:
			self.icon.AddDataRenderer('', 0, '')
		if self.config['quick-info'] == INFO_NONE:
			self.icon.SetQuickInfo('')
	
	##### callbacks #####
	
	def on_click(self,iState):
		Xid = self.icon.Get("Xid")
		if Xid != 0:
			print("show window...")
			if self.bHasFocus:
				self.icon.ActOnAppli("minimize")
			else:
				self.icon.ActOnAppli("show")
		else:  # Transmission not started, or in the systray.
			print("launch Transmission...")
			subprocess.Popen(self.cClass)
	
	def on_middle_click(self):
		self.show_torrents_info()
	
	def on_build_menu(self):
		if self.iSessionID != 0:
			items = [ {
					"label": _("Pause all torrents"),
					"icon" : "gtk-media-pause",
					"menu" : CDApplet.MAIN_MENU_ID,
					"id"   : 1
				}, {
					"label": _("Resume all torrents"),
					"icon" : "gtk-media-play",
					"menu" : CDApplet.MAIN_MENU_ID,
					"id"   : 2
				}, {
					"label": _("Torrents info") + " (" + _("middle-click") + ")",
					"icon" : "gtk-info",
					"menu" : CDApplet.MAIN_MENU_ID,
					"id"   : 3
				}, {
					"label": _("Quit"),
					"icon" : "gtk-quit",
					"menu" : CDApplet.MAIN_MENU_ID,
					"id"   : 4
				} ]
			self.icon.AddMenuItems(items)
		
	def on_menu_select(self,iNumEntry):
		if iNumEntry == 1:
			client.core.pause_all_torrents()
		elif iNumEntry == 2:
			client.core.resume_all_torrents()
		elif iNumEntry == 3:
			self.show_torrents_info()
		elif iNumEntry == 4:
			self.close_session()
	
	def on_drop_data(self,cReceivedData):
		print("*** received",cReceivedData)
		# Transmission is quite buggy here: sometimes it doesn't take the request into account (timeout in the http query); when launching transmission with an URL, it adds the URL after 1 minute.
		if self.iSessionID != 0:
			self._request('torrent-add', {'filename':cReceivedData})
		else:
			popen(self.cClass+" "+cReceivedData+"&")
	
	def on_shortkey(self,key):
		self.show_torrents_info()
		
	def on_change_focus(self,has_focus):
		self.bHasFocus = has_focus
	
############
### main ###
############
if __name__ == '__main__':
	Applet().run()
