#!/usr/bin/env python
"""
# pbmpcd.py
# (c) Dub Spencer - http://arton.cunst.net/mail.html?pbmpcd
# mpd / bemused bridge
#
# licensed to you under terms of the
# GNU GENERAL PUBLIC LICENSE, Version 2, June 1991.


O P T I O N S

The default is to assume, that both the mpd server and
the bemused client use the same character encoding: ie.
utf8. This can be changed with two commandline switches
(turning on both will cancel conversion):

-b to say, that the bemused client is using latin1
-m to say, that the mpd server is using latin1
"""

"""
# Debian instructions:
apt-get install mpd python-mpdclient python-bluez
cp pbmpcd.py /usr/local/bin/pbmpcd
chmod 755 /usr/local/bin/pbmpcd

# enter these lines in some init.d startup script
# start line enables latin1 conversion for the midlet!
start-stop-daemon -K -c mpd -m -p /var/run/pbmpcd.pid
start-stop-daemon -S -c mpd -m -p /var/run/pbmpcd.pid \
	-b -x /usr/local/bin/pbmpcd -- -b
"""

from bluetooth import *
from struct import *
import mpdclient2
import sys, os
import syslog
import socket
import time


## U T I L S

def message(msg):
	"""Print a message to stdout and syslog."""
	print msg
	syslog.syslog(msg)

def fatal(msg):
	"""Print a message to stdout and syslog. Stop execution."""
	print msg
	syslog.syslog(msg)
	try:
		client_sock.close()
		server_sock.close()
	finally:
		sys.exit(1)

def recode(str, dir):
	"""Convert a string from mpd to bemused character encoding &vv."""
	if (bcs == mcs):
		return str
	if (dir == 'm2b'): # mpd to bemused
		str = unicode(str, mcs)
		str = str.encode(bcs, 'replace')
	if (dir == 'b2m'): # bemused to mpd
		str = unicode(str, bcs)
		str = str.encode(mcs, 'replace')
	return str

def track_title_short(t):
	"""Get a track's name/title for the playlist"""
	a = ''
	try: # for streams, use the name
		a = t.name
	except AttributeError:
		try: # else use the title
			a = t.title
		except AttributeError:
			pass
	if len(a) == 0:
		try: # use file basename
			a = os.path.basename(t.file)
		except AttributeError:
			pass
	if len(a) == 0: # again, streams
		try: # use filename
			a = t.file
		except AttributeError:
			pass
	if len(a) == 0: # who knows...
		a = '###'
	return a

def track_title_full(t):
	"""Get a track's artist/name/title for the track info"""
	a = b = ''
	try:
		a = t.artist
	except AttributeError:
		try: # streams may have a name instead
			a = t.name
		except AttributeError:
			pass
	if len(a) == 0:
		try: # use file basename
			a = os.path.basename(t.file)
		except AttributeError:
			pass
	if len(a) == 0: # again, streams
		try: # use filename
			a = t.file
		except AttributeError:
			pass
	if len(a) == 0: # who knows...
		a = '###'
	try:
		b = t.title
	except AttributeError:
		pass
	if len(b) == 0:
		return a
	return a + ' - ' + b

def send_dir(d = ''):
	"""Send a directory's entries to the bemused window; do not recurse."""
	if d == '':
		d = 'MPD'

	# send directory
	ntype = 0 # always root
	ftype = 1 # expanded directory
	code = (ntype<<4) | ftype
	f = '>7sB%dsx' % len(d)
	r = pack(f, 'LISTACK', code, d)
	n = client_sock.send(r)
	if calcsize(f) > n:
		print 'Short LISTACK'

	# send entries
	d = recode(d, 'b2m')
	p = d.replace('\\', '/')
	l = m.lsinfo(p[4:]) # strip "MPD/" prefix
	total = len(l)
	count = 1;
	for t in l:
		# node type
		if total == 1:
			ntype = 2 # only child
		elif count == 1:
			ntype = 1 # child
		elif count == total:
			ntype = 4 # last sibling
		else:
			ntype = 3 # sibling
		# file type
		if t.type == 'directory':
			ftype = 3 # unexpanded directory
			path = t.directory
		elif t.type == 'playlist':
			ftype = 2 # file
			path = t.playlist
		elif t.type == 'file':
			ftype = 2 # file
			path = t.file
		else:
			print 'New Type: ' + t
			continue
		# send entry
		code = (ntype<<4) | ftype
		name = os.path.basename(path)
		name = recode(name, 'm2b')
		f = '>B%dsx' % len(name)
		r = pack(f, code, name)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short DIRENTRY ', name
		count += 1

	# send end of list
	ntype = 255
	f = '>Bx'
	r = pack(f, ntype)
	n = client_sock.send(r)
	if calcsize(f) > n:
		print 'Short LISTEND'


## M A I N

progname = os.path.basename(sys.argv[0])
syslog.openlog(progname, syslog.LOG_PID)

# commandline switches
args = sys.argv[1:]
if '-h' in args:
	print __doc__
	sys.exit(2)
# bemused/midlet charset
bcs  = 'utf-8'
mcs  = 'utf-8'
if '-b' in args:
	bcs = 'latin-1'
if '-m' in args:
	mcs = 'latin-1'

# connect to mpd
try:
	m = mpdclient2.connect()
except:
	fatal('Could not connect to MPD, shutting down.')
message('MPD connected at %s:%d.' % (m.talker.host, m.talker.port))

# setup bluetooth service
try:
	server_sock = BluetoothSocket(RFCOMM)
except:
	fatal('Could not create Bluethooth socket: %s.' % sys.exc_info()[0])
server_sock.bind(('', PORT_ANY))
server_sock.listen(1)
port = server_sock.getsockname()[1]

uuid = '71b5077b-d636-4b47-a0ca-d31e8330db65'
advertise_service(server_sock, 'MPD',
	service_id = uuid,
	service_classes = [uuid, SERIAL_PORT_CLASS],
	profiles = [SERIAL_PORT_PROFILE],
	provider = progname,
	description='MPD service')

message('Waiting for connection on RFCOMM channel %d' % port)
try:
	client_sock, client_info = server_sock.accept()
except (KeyboardInterrupt, SystemExit):
	message('Shut down.')
	sys.exit(0)

message('Accepted connection from %s' % client_info[0])

"""some java implementations seem to hold back a commands argument,
so check and loop and append if necessary"""
data = '';

"""some clients concatenate several commands into one packet,
so check and loop if necessary"""
todo = False;

# process bemused commands
while True:
	try:
		if not todo:
			data = data + client_sock.recv(1024)
		todo = False
	except BluetoothError:
		data = '';
		client_sock.close()
		message('Bluetooth error %s.' % sys.exc_info()[1])
		message('Waiting for connection on RFCOMM channel %d' % port)
		try:
			client_sock, client_info = server_sock.accept()
		except (KeyboardInterrupt, SystemExit):
			break;
		message('Accepted connection from %s' % client_info[0])
		continue
	except KeyboardInterrupt:
		break;
	except:
		fatal('Uncaught error in recv: %s.' % sys.exc_info()[0])

	# bemused commands with arguments
	cwa = ('DLST', 'DOWN', 'FINF', 'LADD', 'PLAY', 'REPT', 'SEEK', 'SHFL',
		'SLCT', 'REPT', 'VOLM')
	# loop if argument is missing
	if data in cwa:
		print 'Short %s' % repr(data)
		continue

	### debug
	print '[%s]' % repr(data)

	try:
		m.ping()
	except EOFError:
		message('MPD connection lost, trying to reconnect.')
		try:
			m = mpdclient2.connect()
		except:
			fatal('Could not connect to MPD, shutting down.')
		message('MPD connected at %s:%d.'
			% (m.talker.host, m.talker.port))
	except:
		fatal('Uncaught MPD error: %s.' % sys.exc_info()[0])

	# Bemused Command
	cmd = data[0:4]
	# send status OK
	if cmd == 'CHCK':
		f = '>c'
		r = pack(f, 'Y')
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short CHCKACK'
	# send song info
	elif cmd == 'INFO' or cmd == 'INF2':
		s = m.status()
		if s.state == 'stop':
			st = 0 # state stop
			tc = 0 # song time
			ts = 0 # song length
		elif s.state == 'pause':
			st = 0 # state pause
			tc, ts = s.time.split(':', 2)
		else: # play
			st = 1 # state play
			tc, ts = s.time.split(':', 2)
		title = track_title_full(m.currentsong())
		title = recode(title, 'm2b')
		if cmd == 'INFO':
			f = '>7sBIIBB%ds' % len(title)
			a = 'INFOACK'
		else: # INF2, null terminate string
			f = '>7sBIIBB%dsx' % len(title)
			a = 'INF2ACK'
		r = pack (f, a, st, int(ts), int(tc),
			int(s.random), int(s.repeat), title)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short', a
	# send playback volume
	elif cmd == 'GVOL':
		s = m.status()
		f = '>7sB'
		v = int(s.volume) * 255 / 100
		r = pack(f, 'GVOLACK', v)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short GVOLACK'
	# send current playlist, each entry as a single chunk
	elif cmd == 'PLST':
		s = m.status()
		if s.state == 'stop':
			p = 0
		else:
			p = int(s.song)
		l = m.playlistinfo()
		# send header
		f = '>7sH2s'
		r = pack(f, 'PLSTACK', p, '#\n')
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short PLSTACK1'
		# send entries
		for t in l:
			title = track_title_short(t) + '\n'
			title = recode(title, 'm2b')
			f = '>%ds' % len(title)
			r = pack(f, title)
			n = client_sock.send(r)
			if calcsize(f) > n:
				print 'Short PLSTACK2'
		# send end
		f = '>x'
		r = pack(f)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short PLSTACK3'
	# send playlist length
	elif cmd == 'PLEN':
		l = m.playlistinfo()
		c = len(l)
		f = '>h'
		r = pack(f, c)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short PLENACK'
	# start playback
	elif cmd == 'STRT':
		s = m.status()
		if s.state == 'pause':
			m.pause(False)
		else:
			m.play()
	# stop playback
	elif cmd == 'STOP' or cmd == 'STEN':
		m.stop()
	# pause playback
	elif cmd == 'PAUS' or cmd == 'FADE':
		s = m.status()
		if s.state == 'pause':
			m.pause(False)
		else:
			m.pause(True)
	# play next track
	elif cmd == 'NEXT':
		m.next()
	# play previous track
	elif cmd == 'PREV':
		m.previous()
	# wind 5 secs
	elif cmd == 'FFWD':
		s = m.status()
		if s.state == 'stop':
			data = ''
			continue
		tc, ts = s.time.split(':', 2)
		tw = int(tc) + 5
		m.seekid(int(s.songid), tw)
	# rewind 5 secs
	elif cmd == 'RWND':
		s = m.status()
		if s.state == 'stop':
			data = ''
			continue
		tc, ts = s.time.split(':', 2)
		tw = int(tc) - 5
		m.seekid(int(s.songid), tw)
	# set playback volume
	elif cmd == 'VOLM':
		f = '>4sB'
		try:
			k, v = unpack(f, data)
			m.setvol(v * 100 / 255)
		except:
			print 'Not a command [%s]' % repr(data)
	# select a track from playlist
	elif cmd == 'SLCT':
		f = '>4sH'
		try:
			k, v = unpack(f, data)
			m.play(v)
		except:
			print 'Not a command [%s]' % repr(data)
	# toggle shuffle/random play
	elif cmd == 'SHFL':
		f = '>4sB'
		try:
			k, v = unpack(f, data)
			m.random(v)
		except:
			print 'Not a command [%s]' % repr(data)
	# toggle repeat play
	elif cmd == 'REPT':
		f = '>4sB'
		try:
			k, v = unpack(f, data)
			m.repeat(v)
		except:
			print 'Not a command [%s]' % repr(data)
	# clear the playlist
	elif cmd == 'RMAL':
		m.clear()
	# list a directory
	elif cmd == 'DLST':
		d = str(data[6:])
		send_dir(d)
	# list root directory
	elif cmd == 'LIST':
		send_dir()
	# append a file, playlist or directory
	elif cmd == 'LADD':
		d = str(data[6:])
		d = d.replace('\\', '/')
		d = recode(d, 'b2m');
		s = m.status()
		id = s.playlist
		m.add(d[4:]) # strip "MPD/" prefix
		n = m.plchanges(int(id))
		if not n:
			print 'Not a track [%s]' % repr(d)
	# play tracks/lists from db
	elif cmd == 'PLAY':
		d = str(data[6:])
		d = d.replace('\\', '/')
		d = recode(d, 'b2m');
		s = m.status()
		id = s.playlist
		m.add(d[4:]) # strip "MPD/" prefix
		n = m.plchanges(int(id))
		if n:
			m.playid(int(n[0].id))
		else:
			print 'Not a track [%s]' % repr(d)
	# send script version
	elif cmd == 'VERS':
		f = '>7s2B'
		r = pack(f, 'VERSACK', 1, 73)
		n = client_sock.send(r)
		if calcsize(f) > n:
			print 'Short VERSACK'
	# quit running script
	elif cmd == 'SHUT':
		message('Shut down request received.')
		break
	# unknown
	else:
		print 'New command [%s]' % repr(data)

	# loop if command pending
	if cmd not in cwa:
		if len(data) > 4:
			todo = True
			data = data[4:]
			continue
	data = ''

try:
	client_sock.close()
	server_sock.close()
finally:
	message('Shut down.')
	sys.exit(0)
