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