#!/usr/bin/python
#
#    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 3 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.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Author: Erik Karlsson <pilo@ayeon.org>

import os
import sys
import signal
import string
import socket
import getopt

import mpd
import gobject
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import ConfigParser

### BEGIN CONFIGURATION ###

# Default parameters, change these to change default behaviour
params = { 
	'host': 'localhost',
	'port': 6600,
	'password': None,
	'progname': sys.argv[0]
}

## END CONFIGURATION - DO NOT CHANGE ANYTHING BELOW THIS LINE ###

# MPRIS allowed metadata tags
allowed_tags = {
	'title': str,
	'artist': str,
	'album': str,
	'tracknumber': str,
	'time': int,
	'mtime': int,
	'genre': str,
	'comment': str,
	'rating': int,
	'year': int,
	'date': int,
	'location': str,
	'arturl': str,
	'asin': str,
	'puid fingerprint': str,
	'mb track id': str,
	'mb artist id': str,
	'mb artist sort name': str,
	'mb album id': str,
	'mb release date': str,
	'mb album artist': str,
	'mb album artist id': str,
	'mb album artist sort name': str,
	'audio-bitrate': int,
	'audio-samplerate': int,
	'video-bitrate': int
}

# MPRIS capabilites
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

# Default url handlers if MPD doesn't support 'urlhandlers' command
urlhandlers = [ 'http://' ]

def FormatMetadata(metadata, path):
	if 'date' in metadata:
		if len(metadata['date']) is 4:
			metadata['year'] = metadata['date']
		else:
			metadata['year'] = metadata['date'][0:4]
		del metadata['date']
	
	if 'track' in metadata:
		metadata['tracknumber'] = metadata['track']
		del metadata['track']
		if 'disc' in metadata:
			metadata['tracknumber'] = metadata['tracknumber'] + '/' + metadata['disc']
			del metadata['disc']

	if 'file' in metadata:
		file = metadata['file']
		# prepend path to library if it isn't a uri
		if len([ x for x in urlhandlers if file.startswith(x) ]) == 0:
			file = os.path.join(path, file)
		metadata['location'] = file
		del metadata['file']

	# Stream: populate some missings tags with stream's name
	if 'name' in metadata:
		if 'title' not in metadata:
			metadata['title'] = metadata['name']
		elif 'album' not in metadata:
			metadata['album'] = metadata['name']

	surplus_tags = set(metadata.keys()).difference(set(allowed_tags.keys()))

	# Remove surplus tags
	for tag in surplus_tags:
		del metadata[tag]
	
	# Cast metadata to the correct type, or discard it
	for key, value in metadata.items():
		try:
			metadata[key] = allowed_tags[key](value)
		except ValueError, e:
			del metadata[key]
			# FIXME
			print e
	
	return metadata


# Wrapper to handle socket errors and similar
class MPDWrapper:
	def __init__(self, mpd_client):
		self.__mpd_client = mpd_client
	
	def __getattr__(self, name):
		try:
			result = getattr(self.__mpd_client, name)
		except (socket.error, mpd.MPDError):
			self.__mpd_client.disconnect()
			raise dbus.DBusException

		return result


class MPRISRoot(dbus.service.Object):
	''' The base object of an MPRIS player '''
	def __init__(self, bus):
		dbus.service.Object.__init__(self, bus, '/')
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 's')
	def Identity(self):
		return 'MPD ' + mpd_wrapper.mpd_version

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Quit(self):
		print 'Aborting at client request.'
		loop.quit()
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '(qq)')
	def MprisVersion(self):
		return (dbus.UInt16(1), dbus.UInt16(0))


class MPRISTrackList(dbus.service.Object):
	''' Class describing a Track List '''
	def __init__(self, bus, path):
		dbus.service.Object.__init__(self, bus, '/TrackList')
		self.__path = path
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'i', out_signature = 'a{sv}')
	def GetMetadata(self, index):
		return FormatMetadata(mpd_wrapper.playlistinfo(index)[0], self.__path)
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'i')
	def GetCurrentTrack(self):
		return dbus.Int32(mpd_wrapper.currentsong()['id'])
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'i')
	def GetLength(self):
		return dbus.Int32(mpd_wrapper.status()['playlistlength'])
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'sb', out_signature = 'i')
	def AddTrack(self, track, play_immediately):
# TODO: Is this even possible?
		raise dbus.DBusException
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'i', out_signature = '')
	def DelTrack(self, index):
		mpd_wrapper.deleteid(index)
		return
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'b', out_signature = '')
	def SetLoop(self, value):
		''' NOP, since MPD does not support this '''
# TODO: Exception instead?
		return
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'b', out_signature = '')
	def SetRandom(self, value):
		mpd_wrapper.random(value)
		return

	@dbus.service.signal('org.freedesktop.MediaPlayer', signature = 'i')
	def TrackListChange(self, length):
		return


class MPRISPlayer(dbus.service.Object):
	def __init__(self, bus, path):
		dbus.service.Object.__init__(self, bus, '/Player');
		self.__path = path
	
	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Next(self):
		mpd_wrapper.next()
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Prev(self):
		mpd_wrapper.previous()
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Pause(self):
		mpd_wrapper.pause()
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Stop(self):
		mpd_wrapper.stop()
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '')
	def Play(self):
		mpd_wrapper.play()
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'b', out_signature = '')
	def Repeat(self, value):
		mpd_wrapper.repeat(value)
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = '(iiii)')
	def GetStatus(self):
		status = mpd_wrapper.status()
		if status['state'] == 'play':
			play_status = 0
		elif status['state'] == 'pause':
			play_status = 1
		elif status['state'] == 'stop':
			play_status = 2

		return (play_status, dbus.Int32(status['random']), dbus.Int32(status['repeat']), 0) 

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'a{sv}')
	def GetMetadata(self):
		return FormatMetadata(mpd_wrapper.currentsong(), self.__path)

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'i')
	def GetCaps(self):
		caps = CAN_HAS_TRACKLIST

		status = mpd_wrapper.status()
		if int(status['playlistlength']) != 0:
			caps |= CAN_GO_NEXT | CAN_GO_PREV | CAN_PAUSE | CAN_PLAY | CAN_SEEK | CAN_PROVIDE_METADATA

		return dbus.Int32(caps)

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'i', out_signature = '')
	def VolumeSet(self, volume):
		mpd_wrapper.setvol(int(volume))
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'i')
	def VolumeGet(self):
		status = mpd_wrapper.status()
		return dbus.Int32(status['volume'])

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = 'i', out_signature = '')
	def PositionSet(self, position):
		status = mpd_wrapper.status()
		if status['state'] == 'stop':
			return

		mpd_wrapper.seek(status['song'], int(position / 1000))
		return

	@dbus.service.method('org.freedesktop.MediaPlayer', in_signature = '', out_signature = 'i')
	def PositionGet(self):
		status = mpd_wrapper.status()
		if status['state'] == 'stop':
			return 0
		else:
			return dbus.Int32(status['time'].split(':', 1)[0]) * 1000
	
	@dbus.service.signal('org.freedesktop.MediaPlayer', signature = 'a{sv}')
	def TrackChange(self, metadata):
		return
	
	@dbus.service.signal('org.freedesktop.MediaPlayer', signature = '(iiii)')
	def StatusChange(self, status):
		return

	@dbus.service.signal('org.freedesktop.MediaPlayer', signature = 'i')
	def CapsChange(self, caps):
		return


# Handle signals more gracefully
def handle_sigint(signum, frame):
	print 'Caught SIGINT, aborting.'
	loop.quit()

# Periodic status check function
def check_mpd_status(mpd_client, host, port, password, track_list, player):
	# TODO: This should perhaps be exception-checked?
	status = mpd_client.status()

	# Invalidate some fields, so that we throw out events at start
	status['state'] = 'invalid'
	status['songid'] = -1

	song = mpd_client.currentsong()
	is_stream = 'name' in song

	while True:
		old_status = status
		try:
			status = mpd_client.status()
		except (socket.error, mpd.MPDError):
			# Command, failed - try to reconnect
			while True:
				try:
					# Clean out any bad socket FDs before trying to connect..
					# this might leave stray FDs!
					mpd_client._reset()
					mpd_client.connect(host, port)
					if password:
						mpd_client.password(password)
				except (socket.error, mpd.MPDError), e:
					yield old_status
					# Retry..
					continue

				break

			# Successful reconnection
			pass

		if status['state'] != 'stop' and \
		   old_status['state'] != 'stop':
			if old_status['songid'] != status['songid']:
				player.TrackChange(player.GetMetadata())
				is_stream = 'name' in mpd_client.currentsong()
			# Stream: can provide song's metadata, check it
			elif is_stream:
				old_song = song
				song = mpd_client.currentsong()
				if song.get('title') != old_song.get('title') or \
				   song.get('artist') != old_song.get('artist') or \
				   song.get('album') != old_song.get('album'):
					player.TrackChange(player.GetMetadata())

		if old_status['state'] != status['state'] or \
		   old_status['random'] != status['random'] or \
		   old_status['repeat'] != status['repeat']:
			player.StatusChange(player.GetStatus())
			if old_status['state'] == 'stop':
				player.TrackChange(player.GetMetadata())

		if 0 == int(status['playlistlength']):
			player.CapsChange(player.GetCaps())

		if old_status['playlist'] != status['playlist']:
			track_list.TrackListChange(track_list.GetLength())

		yield status

def host_and_password(str):
	parts = str.partition('@')
	return (parts[2], parts[0])

def usage(dict):
    print """\
Usage: %(progname)s [OPTION]... [MPD_HOST] [MPD_PORT]
	
Note: Environment variables MPD_HOST and MPD_PORT can be used instead of above
      arguments.

	 -p, --path=PATH        Sets the library path of MPD to PATH
	
Default: MPD_HOST: %(host)s, MPD_PORT: %(port)s

Report bugs to <pilo@ayeon.org>""" % dict

if __name__ == '__main__':
	DBusGMainLoop(set_as_default=True)

	path = ''

	try:
		(opts, args) = getopt.getopt(sys.argv[1:], 'hp:', ['help', 'path='])
	except getopt.GetoptError, (msg, opt):
		print sys.argv[0] + ': ' + msg
		print
		usage(params)
		sys.exit(2)
	
	for (opt, arg) in opts:
		if opt in ['-h', '--help']:
			usage(params)
			sys.exit()
		elif opt in ['-p', '--path']:
			path = arg
	
	config = ConfigParser.SafeConfigParser()
	config.read(['/etc/mpDris.conf', os.path.expanduser('~/.config/mpDris/mpDris.conf')])
	
	if config.has_option('Connection', 'host'):
		params['host'] = config.get('Connection', 'host')
	if config.has_option('Connection', 'port'):
		params['port'] = config.get('Connection', 'port')
	if config.has_option('Connection', 'password'):
		params['password'] = config.get('Connection', 'password')


	if 'MPD_HOST' in os.environ:
		params['host'] = os.environ['MPD_HOST']
	if 'MPD_PORT' in os.environ:
		params['port'] = os.environ['MPD_PORT']

	if len(args) > 2:
		usage(params)
		sys.exit()

	for arg in args[:2]:
		if arg.isdigit():
			params['port'] = arg
		else:
			params['host'] = arg
	
	if '@' in params['host']:
		(params['host'], params['password']) = host_and_password(params['host'])

	if not len(path):
		print 'Warning: By not supplying a path for the music library ' \
		      'this program will break the MPRIS specification!'
	elif not path.startswith('file://'):
		path = 'file://' + path

	loop = gobject.MainLoop()

	signal.signal(signal.SIGINT, handle_sigint)

	# Init DBUS connection
	session_bus = dbus.SessionBus()
	name = dbus.service.BusName('org.mpris.mpd', session_bus)

	root = MPRISRoot(session_bus)
	track_list = MPRISTrackList(session_bus, path)
	player = MPRISPlayer(session_bus, path)

	# Init MPD connection
	mpd_client = mpd.MPDClient()
	try:
		mpd_client.connect(params['host'], params['port'])
		if params['password']:
			mpd_client.password(params['password'])
	except socket.error, e:
		print 'Fatal: Could not connect to MPD: ' + str(e)
		sys.exit(2)
	except mpd.CommandError, e:
		print 'Fatal: MPD command error: ' + str(e)
		sys.exit(2)

	# Get URL handlers supported by MPD
	if 'urlhandlers' in mpd_client.commands():
		urlhandlers = mpd_client.urlhandlers()

	# Create wrapper to handle connection failures with MPD more gracefully..
	# i.e. throw another kind of exception.. :P
	mpd_wrapper = MPDWrapper(mpd_client)

	# Add periodic status check for MPRIS signals
	gobject.timeout_add(1000, check_mpd_status(mpd_client, params['host'], params['port'], params['password'], track_list, player).next)
	
	# Run idle loop
	loop.run()

	# Clean up
	try:
		mpd_client.close()
		mpd_client.disconnect()
	except mpd.ConnectionError:
		pass
