#!/usr/bin/env python

#  xmms2-mpris-bridge - MPRIS interface for XMMS2
#  Copyright (C) 2008-2009 Thomas Frauendorfer

"""
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""

import sys
try:
	import xmmsclient
	import xmmsclient.glib
	from xmmsclient.consts import *
	import dbus
	import dbus.service
	from dbus.mainloop.glib import DBusGMainLoop
	import gobject
	import urllib
except ImportError:
	print "Could not start the XMMS2 MPRIS bridge."
	print "Please install python bindings for xmms2, dbus and gobject"
	sys.exit(1)

MPRIS = "org.mpris.xmms2"
MEDIAPLAYER = "org.freedesktop.MediaPlayer"

# The name reported to dbus
DBUS_CLIENTNAME = "XMMS2_MPRIS"
# The name reported to xmms2, only alphanumeric caracters
XMMS2_CLIENTNAME = 'MPRIS_bridge'

NONE                 = 0
CAN_GO_NEXT          = 1 << 0
CAN_GO_PREV          = 1 << 1
CAN_PAUSE            = 1 << 2
CAN_PLAY             = 1 << 3
CAN_SEEK             = 1 << 4
CAN_PROVIDE_METADATA = 1 << 5
CAN_HAS_TRACKLIST    = 1 << 6

DBUS_PLAY  = 0
DBUS_PAUSE = 1
DBUS_STOP  = 2

#FIXME: Those three status informations are not yet implemented
DBUS_RANDOM_OFF = 0
DBUS_RANDOM_ON  = 1

DBUS_REPEAT_CURRENT_OFF = 0
DBUS_REPEAT_CURRENT_ON  = 1

DBUS_REPEAT_PL_OFF = 0
DBUS_REPEAT_PL_ON  = 1

# Some general functions
def program_error (cb_func, Exception):
	cb_func ("Internal MPRIS-bridge error: " + str (Exception))

#TODO: Add additional fields as soon as xmms2 provides them
def convert_dict (dict):
	# location is mandatory, so no trying and catching
	ret_dict = {'location':urllib.unquote_plus(dict['url'])}

	key_list = ['title', 'artist', 'album', 'genre', 'comment' ]
	for key in key_list:
		try: ret_dict[key] = dict[key]
		except KeyError: pass

	# add later (mpris, xmms2): ('rating', ?), ('year', 'date'), ('date', ?)
	# ('arturl', ?)
	key_list = [('tracknumber', 'tracknr'), ('mtime', 'duration'), \
	            ('audio-bitrate', 'bitrate'), \
	            ('audio-samplerate', 'samplerate')]
	for mpris_key, xmms2_key in key_list:
		try: ret_dict[mpris_key] = dict[xmms2_key]
		except KeyError: pass

	try: ret_dict['time'] = dict['duration'] /1000
	except KeyError: pass

	return ret_dict

class dbusErrorMessage (Exception):
	def __init__ (self, *args, **kwd):
		Exception.__init__ (self, *args, **kws)

# Python magic to allow callback fuctions to use 'return' and exceptions
# TODO: better error formating
def dbus_ret (func, cb_val, cb_err):
	def wrapper (*args, **kws):
		try:
			cb_val (func(*args, **kws))
		except dbusErrorMessage, err:
			cb_err (str (err))
		except Exception, err:
			cb_err ("Internal MPRIS-bridge error: " + str (err))
	return wrapper


# MPRIS '/' handler class
class mpris_root (dbus.service.Object):
	def __init__ (self, xmms2):
		self.xmms2 = xmms2
		dbus.service.Object.__init__ (self, dbus.SessionBus(), "/")

	@dbus.service.method (MEDIAPLAYER, in_signature='', out_signature='s')
	def Identity (self):
		return DBUS_CLIENTNAME

	@dbus.service.method (MEDIAPLAYER, in_signature='', out_signature='')
	def Quit (self):
		self.xmms2.quit ()

	@dbus.service.method (MEDIAPLAYER, in_signature='', out_signature='(qq)')
	def MprisVersion (self):
		return (1, 0)




# MPRIS "/TrackList" handler class
class mpris_TrackList (dbus.service.Object):
	def __init__ (self, xmms2, player):
		dbus.service.Object.__init__ (self, dbus.SessionBus(), "/TrackList")
		self.xmms2 = xmms2
		self.playlist = [] # will be set afterwards
		self.current_pos = 0
		self.active_name =""
		self.player = player
		xmms2.playlist_current_active (self._cb_handle_pls_name)
		xmms2.broadcast_playlist_loaded (cb=self._cb_handle_pls_name)
#		xmms2.playlist_list_entries (cb=self._cb_handle_playlist) #done in _cb_handle_pls_name
		xmms2.broadcast_playlist_changed (cb=self._cb_fetch_playlist)
#		xmms2.playlist_current_pos (cb=self._cb_handle_current_pos) # done in _cb_handle_playlist
		xmms2.broadcast_playlist_current_pos (cb=self._cb_handle_current_pos)
		#xmms2.playback_status (cb=self._cb_playback_status)

	@dbus.service.method (MEDIAPLAYER, in_signature='i', out_signature='a{sv}', \
	                       async_callbacks=('cb_val', 'cb_err'))
	def GetMetadata (self, pos, cb_val, cb_err):
		self.xmms2.medialib_get_info (id=self.playlist[pos], cb=dbus_ret (self._cb_GetMetadata, cb_val, cb_err))

	def _cb_GetMetadata (self, res):
		return convert_dict (res.value ())

	@dbus.service.method (MEDIAPLAYER, out_signature="i")
	def GetCurrentTrack (self):
		return self.current_pos

	@dbus.service.method (MEDIAPLAYER, out_signature="i")
	def GetLength (self):
		return len (self.playlist)

	@dbus.service.method (MEDIAPLAYER, out_signature="i")
	def Clear (self):
		if self.xmms2.playlist_clear(self.active_name):
			return 1
		return 0

	#FIXME: function lies, xmms2 doesn't tell us if adding failed
	@dbus.service.method (MEDIAPLAYER, in_signature='sb', out_signature="i", \
	                      async_callbacks=('cb_val', 'cb_err'))
	def AddTrack (self, url, play_now, cb_val, cb_err):
		if play_now:
			self.xmms2.playlist_add_url (url, cb=dbus_ret (self._cb_AddTrack_now, cb_val, cb_err))
		else:
			self.xmms2.playlist_add_url (url, cb=dbus_ret (self._cb_AddTrack, cb_val, cb_err))

	def _cb_AddTrack (self, res):
		# TODO: errorchecking, but for now the python bindings seem to be broken
		ret = res.value ()
		if ret != None:
			return ret
		else:
			return 0

	def _cb_AddTrack_now (self, res):
		# FIXME: this function does almost certainly not work correct,
		# because something in the python bindings doesn't work correctly
		# For now the playlist update will be called after this method
		# if that changes, the len (self.playlist) has to be adjusted
		self.xmms2.playlist_set_next (len (self.playlist))
		self.xmms2.playback_tickle ()
		# TODO: BetterError handling
		ret = res.value ()
		if ret != None:
			return ret
		else:
			return 0

	@dbus.service.method (MEDIAPLAYER, in_signature="i", out_signature="i")
	def PlayTrack (self, pos):
		if pos <= len (self.playlist):
			self.xmms2.playlist_set_next (pos)
			self.xmms2.playback_tickle ()
			if self.player.playstatus != DBUS_PLAY:
				self.xmms2.playback_start ()
			return 1
		else:
			return 0

	@dbus.service.method (MEDIAPLAYER, in_signature="i")
	def DelTrack (self, pos):
		self.xmms2.playlist_remove_entry (pos)

	@dbus.service.method (MEDIAPLAYER, in_signature="b")
	def SetLoop (self, loop):
		if loop:
			try:
				self.xmms2.config_set_value ("playlist.repeat_all", "1")
			except AttributeError:
				self.xmms2.configval_set ("playlist.repeat_all", "1")
		else:
			try:
				self.xmms2.config_set_value ("playlist.repeat_all", "0")
			except AttributeError:
				self.xmms2.configval_set ("playlist.repeat_all", "0")

	#TODO: currently not possible, because sorting won't work
	@dbus.service.method (MEDIAPLAYER, in_signature="b")
	def SetRandom (self, random):
		raise NotImplementedError ("Random play is not yet possible in xmms2")

	#Signals
	@dbus.service.signal (MEDIAPLAYER, signature='i')
	def TrackListChange (self, len):
		pass

	# Just fetch the complete playlist whenever it might have canged.
	# This could be optimized, but I'll leave that for sometime later
	def _cb_fetch_playlist (self, res):
		self.xmms2.playlist_list_entries (cb=self._cb_handle_playlist)

	def _cb_handle_playlist (self, res):
		list = res.value ()
		# This method is also called if another playlist is loaded.
		# We handle playlists with the same entries as the same playlist,
		# even if their names differ
		self.xmms2.playlist_current_pos (cb=self._cb_handle_current_pos)
		if (self.playlist != list):
			self.playlist = list
			self.TrackListChange (len (list))

	def _cb_handle_pls_name (self, res):
		name = res.value ()
		if self.active_name != name:
			self.active_name = name
			self.xmms2.playlist_list_entries (cb=self._cb_handle_playlist)


	def _cb_handle_current_pos (self, res):
		pos_d = res.value ()
		try:
			if self.active_name == pos_d['name']:
				self.current_pos = pos_d['position']
		except TypeError:				
			self.current_pos = 0


# MPRIS "/Player" handler class
class mpris_Player (dbus.service.Object):
	# TODO: react to streams: CAN_SEEK = false, CAN_PAUSE = false
	def __init__ (self, xmms2):
		dbus.service.Object.__init__ (self, dbus.SessionBus(), "/Player")
		self.xmms2 = xmms2
		self.onechannel = False # TODO: initialize correctly on stratup
		self.current_id = 0 # This is also returned by xmms2 if no song is currently set
		self.playstatus = DBUS_STOP
		self.randomplay = DBUS_RANDOM_OFF
		self.current_repeat = DBUS_REPEAT_CURRENT_OFF
		self.pl_repeat = DBUS_REPEAT_PL_OFF
		# CAN_SEEK, CAN_PROVIDE_METADATA will be handled later,
		# CAN_PAUSE and CAN_PLAY will also need to be handled according to plystatus
		self.caps = CAN_GO_NEXT | CAN_GO_PREV | CAN_PAUSE | CAN_PLAY \
		          | CAN_HAS_TRACKLIST
		xmms2.playback_current_id (cb=self._cb_current_id)
		xmms2.broadcast_playback_current_id (cb=self._cb_current_id)
		xmms2.broadcast_medialib_entry_changed (cb=self._cb_entry_changed)
		try:
			xmms2.config_list_values (cb=self._cb_handle_repeat)
		except AttributeError:
			xmms2.configval_list (cb=self._cb_handle_repeat)
		try:
			xmms2.broadcast_config_value_changed (cb=self._cb_repeat_changed)
		except AttributeError:
			xmms2.broadcast_configval_changed (cb=self._cb_repeat_changed)
		xmms2.playback_status (cb=self._cb_playback_status)
		xmms2.broadcast_playback_status (cb=self._cb_playback_status)

	@dbus.service.method (MEDIAPLAYER)
	def Next (self):
		self.xmms2.playlist_set_next_rel (1)
		self.xmms2.playback_tickle ()

	@dbus.service.method (MEDIAPLAYER)
	def Prev (self):
		self.xmms2.playlist_set_next_rel (-1)
		self.xmms2.playback_tickle ()

	@dbus.service.method (MEDIAPLAYER)
	def Pause (self):
		self.xmms2.playback_pause ()

	@dbus.service.method (MEDIAPLAYER)
	def Stop (self):
		self.xmms2.playback_stop ()

	@dbus.service.method (MEDIAPLAYER)
	def Play (self):
		self.xmms2.playback_start ()

	#TODO
	@dbus.service.method (MEDIAPLAYER, in_signature='b')
	def Repeat(self, repeat):
		if repeat:
			try:
				self.xmms2.config_set_value ("playlist.repeat_one", "1")
			except AttributeError:
				self.xmms2.configval_set ("playlist.repeat_one", "1")
		else:
			try:
				self.xmms2.config_set_value ("playlist.repeat_one", "0")
			except AttributeError:
				self.xmms2.configval_set ("playlist.repeat_one", "0")

	@dbus.service.method (MEDIAPLAYER, out_signature='(iiii)')
	def GetStatus(self):
		return self.build_status ()


	@dbus.service.method (MEDIAPLAYER, out_signature='a{sv}',
	                      async_callbacks=('cb_val', 'cb_err'))
	def GetMetadata(self, cb_val, cb_err):
		ret = self.xmms2.medialib_get_info (self.current_id, \
		                                    cb=dbus_ret (self._cb_GetMetadata, cb_val, cb_err))

	def _cb_GetMetadata (self, ret):
		dict = ret.value ()
		if (dict == None):
			return {}
		return convert_dict (dict)


	@dbus.service.method (MEDIAPLAYER, out_signature='i')
	def GetCaps (self):
		return self.caps

	@dbus.service.method (MEDIAPLAYER, in_signature='i')
	def VolumeSet (self, volume):
		if (self.onechannel):
			self.xmms2.playback_volume_set ("master", volume)
		else:
			self.xmms2.playback_volume_set ("left", volume)
			self.xmms2.playback_volume_set ("right", volume)


	@dbus.service.method (MEDIAPLAYER, out_signature='i',
	                      async_callbacks=('cb_val', 'cb_err'))
	def VolumeGet (self, cb_val, cb_err):
		self.xmms2.playback_volume_get (cb=dbus_ret (self._cb_VolumeGet, cb_val, cb_err))

	def _cb_VolumeGet (self, res):
		try:
			res = res.value ()
			if (res.has_key ('master')):
				self.onechannel = True
				return res['master']
			else:
				self.onechannel = False
				return ((res['left']+res['right'])/2)
		except KeyError:
			raise dbusErrorMessage ("could not get volume, volume keys: " + str(res.keys ()))


	@dbus.service.method (MEDIAPLAYER, in_signature='i')
	def PositionSet (self, mtime):
		self.xmms2.playback_seek_ms (mtime)


	@dbus.service.method (MEDIAPLAYER, out_signature='i',
	                      async_callbacks=('cb_val', 'cb_err'))
	def PositionGet (self, cb_val, cb_err):
		self.xmms2.playback_playtime (cb=dbus_ret (self._cb_PositionGet, cb_val, cb_err))

	def _cb_PositionGet (self, res):
		return res.value ()

	#Signals
	@dbus.service.signal (MEDIAPLAYER, signature='a{sv}')
	def TrackChange (self, var):
		pass

	@dbus.service.signal (MEDIAPLAYER, signature='(iiii)')
	def StatusChange (self, data):
		pass

	@dbus.service.signal (MEDIAPLAYER, signature='i')
	def CapsChange (self, caps):
		pass

	# class-internal helpers
	def build_status (self):
		return (self.playstatus, self.randomplay, self.current_repeat, self.pl_repeat)

	# Callbacks emitting used for emitting the dbus signals
	def _cb_current_id (self, res):
		self.current_id = res.value ()
		if self.current_id < 1:
			# mlib id 0 means that no entry is selected, so we also have no metadata 
			if self.caps & CAN_PROVIDE_METADATA:
				self.caps &= ~CAN_PROVIDE_METADATA
				self.CapsChange (self.caps)
			return
		if (~self.caps) & CAN_PROVIDE_METADATA:
			self.caps |= CAN_PROVIDE_METADATA
			self.CapsChange (self.caps)
		self.xmms2.medialib_get_info (self.current_id, self._cb_mlib_data)

	def _cb_entry_changed (self, res):
		if (self.current_id == res.value ()):
			self.xmms2.medialib_get_info (self.current_id, self._cb_mlib_data)

	def _cb_mlib_data (self, res):
		res = res.value ()
		self.TrackChange (convert_dict (res))

	def _cb_playback_status (self, res):
		status = res.value ()
		# TODO: Modify CAN_PLAY and CAN_PAUSE
		if (status == PLAYBACK_STATUS_STOP) and (self.playstatus != DBUS_STOP):
			self.playstatus = DBUS_STOP
			self.StatusChange (self.build_status ())
		elif (status == PLAYBACK_STATUS_PLAY) and (self.playstatus != DBUS_PLAY):
			self.playstatus = DBUS_PLAY
			self.StatusChange (self.build_status ())
		elif (status == PLAYBACK_STATUS_PAUSE) and (self.playstatus != DBUS_PAUSE):
			self.playstatus = DBUS_PAUSE
			self.StatusChange (self.build_status ())

	def _cb_repeat_changed (self, res):
		if self._cb_handle_repeat (res):
			self.StatusChange (self.build_status ())
		# Continue to emit broadcast
		return True

	def _cb_handle_repeat (self, res):
		dict = res.value ()
		ret = False
		#FIXME the int (dict) part might break if the value is something like
		# 'False'
		try:
			repeat = int (dict['playlist.repeat_all'])
			if repeat and (self.pl_repeat == DBUS_REPEAT_PL_OFF):
				self.pl_repeat = DBUS_REPEAT_PL_ON
				ret = True
			elif (not repeat) and  (self.pl_repeat == DBUS_REPEAT_PL_ON):
				self.pl_repeat = DBUS_REPEAT_PL_OFF
				ret = True
		except KeyError: pass
		try:
			repeat = int (dict['playlist.repeat_one'])
			if repeat and (self.current_repeat == DBUS_REPEAT_CURRENT_OFF):
				self.current_repeat = DBUS_REPEAT_CURRENT_ON
				ret = True
			elif (not repeat) and (self.current_repeat == DBUS_REPEAT_CURRENT_ON):
				self.current_repeat = DBUS_REPEAT_CURRENT_OFF
				ret = True
		except KeyError: pass
		return ret

class Xmms2MPRIS:
	def __init__(self):
		# The glib mainloop is used to glue the xmms2 and the dbus connection
		# together, because they both provide their own mainloop and I'm
		# too lazy to write a sprecial connector
		self.ml = gobject.MainLoop(None, False)
		self.xmms2 = xmmsclient.XMMS(XMMS2_CLIENTNAME)
		#connect to the xmms2 server, it should be running as it started us
		#TODO: perhaps this could make use of a bi more error handling
		try:
			self.xmms2.connect(disconnect_func = self._on_server_quit)
		except IOError, detail:
			print 'Connection failed:', detail
			sys.exit(1)
		self.xmmsconn = xmmsclient.glib.GLibConnector(self.xmms2)

		self.dbusconn = DBusGMainLoop(set_as_default=True)
		dbus.SessionBus().request_name (MPRIS);

		self.mpris_root = mpris_root (self.xmms2)
		self.mpris_player = mpris_Player (self.xmms2)
		self.mpris_tracklist = mpris_TrackList (self.xmms2, self.mpris_player)


	def	_on_server_quit(self, result):
		self.ml.quit ()


def start_mpris():
	client = Xmms2MPRIS()
	client.ml.run ()
	return client
	
def usage():
	print "Usage: %s [start]" % sys.argv[0]

if __name__ == "__main__":
	if len(sys.argv) == 1:
		start_mpris()
	elif len(sys.argv) == 2:
		if sys.argv[1] == 'start':
			start_mpris()
		else:
			usage()

	else:
		usage()	
