From ed038868f9465e051f789623e57368e146443929 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Mon, 7 Sep 2009 17:14:11 -0800 Subject: [PATCH] preliminary version of the audio and slink modules... should include a sample asrc file.. [git-p4: depot-paths = "//depot/": change = 1380] --- README | 4 + audio.py | 275 +++++++++++++ cddb.py | 44 ++ pymeds.py | 2 + slinke.py | 1085 ++++++++++++++++++++++++++++++++++++++++++++++++++ slinkemod.py | 559 ++++++++++++++++++++++++++ 6 files changed, 1969 insertions(+) create mode 100644 audio.py create mode 100644 cddb.py create mode 100644 slinke.py create mode 100644 slinkemod.py diff --git a/README b/README index 255f3e1..39c3db8 100644 --- a/README +++ b/README @@ -35,6 +35,7 @@ The following packages are required to run the media server: Optional software packages: * rarfile - http://grue.l-t.ee/~marko/src/rarfile/ * python-shoutcast - http://excentral.org/tarballs/pyshout/python-shoutcast/ + * CDDB-py - http://cddb-py.sourceforge.net/ Thanks to Coherence for soap_lite that solved the issues w/ PS3 not seeing the media server. The PS3 with the latest firmware (2.50 and later) now @@ -99,6 +100,9 @@ v0.x: creating the files. If we get an error parsing the genres of ShoutCAST, try again. Print out the modules that failed to load. + Added an Audio module that will stream audio using the ossaudiodev + module. + Added SLink module for accessing Sony CD Changers. Add support for WAX/ASX files in shoutcast. v0.5: diff --git a/audio.py b/audio.py new file mode 100644 index 0000000..bdcf7cb --- /dev/null +++ b/audio.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +# Copyright 2009 John-Mark Gurney +'''Audio Source''' + +__version__ = '$Change: 1366 $' +# $Id: //depot/python/pymeds/main/shoutcast.py#24 $ + +import ossaudiodev +import os.path + +from DIDLLite import Container, MusicGenre, AudioItem, Resource, ResourceList +from FSStorage import registerklassfun + +from twisted.internet.abstract import FileDescriptor +from twisted.internet import fdesc +from twisted.python import log, threadable, failure +from twisted.web import error, http, resource, server + +from zope.interface import implements + +mttobytes = { + 'audio/l8': 1, + 'audio/l16': 2, +} + +def bytespersecmt(mt): + tmp = [ x.strip() for x in mt.split(';') ] + try: + r = mttobytes[tmp[0].lower()] + except KeyError: + raise ValueError('invalid audio type: %s' % `tmp[0]`) + + v = set(('rate', 'channels')) + for i in tmp[1:]: + arg, value = [ x.strip() for x in i.split('=', 1) ] + if arg in v: + v.remove(arg) + r *= int(value) + else: + raise ValueError('invalid audio parameter %s in %s' % + (`arg`, `mt`)) + + return r + +class AudioPlayer(FileDescriptor): + def __init__(self, consumer, dev, mode, params): + self._dev = ossaudiodev.open(dev, mode) + + # Set some sub-functions + self.fileno = self._dev.fileno + self.setparameters = self._dev.setparameters + + res = self.setparameters(*params) + self._dev.nonblock() + + FileDescriptor.__init__(self) + + self.connected = True + self.attached = consumer + self.writefun = self.attached.write + consumer.registerProducer(self, True) + self.dobuffer = False + self.buffer = None + self.startReading() + + # Drop our useless write connection + self._writeDisconnected = True + + def writeSomeData(self, data): + print 'wsd:', len(data) + return fdesc.writeToFD(self.fileno(), data) + + def doRead(self): + return fdesc.readFromFD(self.fileno(), self.writefun) + + def connectionLost(self, reason): + FileDescriptor.connectionLost(self, reason) + + print 'AP, connectionLost' + self.fileno = lambda: -1 + self.setparameters = None + if self._dev is not None: + self._dev.close() + self._dev = None + self.attached = None + + def stopProducing(self): + print 'AP, sp' + self.writefun = lambda x: None + FileDescriptor.stopProducing(self) + + def pauseProducing(self): + if not self.dobuffer: + self.buffer = [] + self.dobuffer = True + self.writefun = self.buffer.append + #FileDescriptor.pauseProducing(self) + + def resumeProducing(self): + if self.dobuffer: + self.attached.write(''.join(self.buffer)) + self.dobuffer = False + self.buffer = None + self.writefun = self.attached.write + #FileDescriptor.resumeProducing(self) + + def __repr__(self): + return '' % (self.fileno(), self.connected, self.disconnecting, self._writeDisconnected) +class AudioResource(resource.Resource): + isLeaf = True + mtformat = { + ossaudiodev.AFMT_S16_BE: 'audio/L16', + ossaudiodev.AFMT_U8: 'audio/L8', + } + producerFactory = AudioPlayer + + def __init__(self, dev, default): + resource.Resource.__init__(self) + self.dev = dev + self.default = default + + @staticmethod + def getfmt(fmt): + return getattr(ossaudiodev, 'AFMT_%s' % fmt) + + def getmimetype(self, *args): + if len(args) == 0: + args = self.default + elif len(args) != 3: + raise TypeError('getmimetype() takes exactly 0 or 3 aruments (%d given)' % len(args)) + + fmt, nchan, rate = args + origfmt = fmt + try: + fmt = getattr(ossaudiodev, 'AFMT_%s' % fmt) + nchan = int(nchan) + rate = int(rate) + except AttributeError: + raise ValueError('Invalid audio format: %s' % `origfmt`) + + try: + mt = self.mtformat[fmt] + except KeyError: + raise KeyError('No mime-type for audio format: %s.' % + `origfmt`) + + return '%s;rate=%d;channels=%d' % (mt, rate, nchan) + + def render(self, request): + default = self.default + if request.postpath: + default = request.postpath + + fmt, nchan, rate = default + nchan = int(nchan) + rate = int(rate) + try: + request.setHeader('content-type', + self.getmimetype(fmt, nchan, rate)) + except (ValueError, AttributeError, KeyError), x: + return error.ErrorPage(http.UNSUPPORTED_MEDIA_TYPE, + 'Unsupported Media Type', str(x)).render(request) + + #except AttributeError: + # return error.NoResource('Unknown audio format.').render(request) + #except ValueError: + # return error.NoResource('Unknown channels (%s) or rate (%s).' % (`nchan`, `rate`)).render(request) + #except KeyError: + # return error.ErrorPage(http.UNSUPPORTED_MEDIA_TYPE, + # 'Unsupported Media Type', + # 'No mime-type for audio format: %s.' % + # `fmt`).render(request) + + if request.method == 'HEAD': + return '' + + self.producerFactory(request, self.dev, 'r', + (self.getfmt(fmt), nchan, rate, True)) + + # and make sure the connection doesn't get closed + return server.NOT_DONE_YET + + synchronized = [ 'render' ] +threadable.synchronize(AudioResource) + +class ossaudiodev_fmts: + pass + +for i in (k for k in dir(ossaudiodev) if k[:5] == 'AFMT_' and \ + isinstance(getattr(ossaudiodev, k), (int, long))): + setattr(ossaudiodev_fmts, i, getattr(ossaudiodev, i)) + +class AudioSource(AudioItem): + def __init__(self, *args, **kwargs): + file = kwargs.pop('file') + fargs = eval(open(file).read().strip(), { '__builtins__': {}, }) + # 'ossaudiodev': ossaudiodev_fmts }) + self.dev = fargs.pop('dev') + default = fargs.pop('default') + kwargs['content'] = AudioResource(self.dev, default) + AudioItem.__init__(self, *args, **kwargs) + + if False: + self.bitrate = bitrate + + self.url = '%s/%s' % (self.cd.urlbase, self.id) + self.res = ResourceList() + self.res.append(Resource(self.url, 'http-get:*:%s:*' % + kwargs['content'].getmimetype())) + # XXX - add other non-default formats + +def getfmtstrings(f): + r = [] + for i in ( x for x in dir(ossaudiodev) if x[:5] == 'AFMT_' ): + val = getattr(ossaudiodev, i) + if val & f: + f &= ~val + r.append(i) + + while f: + print f, f & -f + r.append(f & -f) + f ^= f & -f + + return r + +def detectaudiosource(origpath, fobj): + path = os.path.basename(origpath) + ext = os.path.splitext(path)[1] + if ext == '.asrc': + return AudioSource, { 'file': origpath } + + return None, None +registerklassfun(detectaudiosource) + +from zope.interface import implements +from twisted.internet.interfaces import IConsumer + +class FileConsumer: + implements(IConsumer) + + def __init__(self, fp): + self.fp = open(fp, 'w') + self.producer = None + + def registerProducer(self, producer, streaming): + if self.producer is not None: + raise RuntimeError('already have a producer') + self.streaming = streaming + self.producer = producer + producer.resumeProducing() + + def unregisterProducer(self): + if self.producer is None: + raise RuntimeError('none registered') + + self.producer = None + + def write(self, data): + self.fp.write(data) + +if __name__ == '__main__': + if False: + i = ossaudiodev.open('/dev/dsp2', 'r') + print getfmtstrings(i.getfmts()) + i.setparameters(ossaudiodev.AFMT_S16_BE, 2, 44100, True) + + print `i.read(16)` + else: + aplr = AudioPlayer('/dev/dsp2', 'r', + (ossaudiodev.AFMT_S16_BE, 2, 44100, True)) + file = FileConsumer('test.output') + file.registerProducer(aplr, True) + from twisted.internet import reactor + reactor.run() diff --git a/cddb.py b/cddb.py new file mode 100644 index 0000000..a45493d --- /dev/null +++ b/cddb.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +def checksum(n): + ret = 0 + + while n > 0: + n, m = divmod(n, 10) + ret += m + + return ret + +def discid(tracks): + '''Pass in a list of tuples. Each tuple must contain +(minutes, seconds) of the track. The last tuple in the list should +contain the start of lead out.''' + + last = len(tracks) - 1 + tracksms = map(lambda x: x[0] * 60 + x[1], tracks) + n = sum(map(checksum, tracksms[:-1])) + + t = tracksms[-1] - tracksms[0] + + discid = (long(n) % 0xff) << 24 | t << 8 | last + + ret = [ discid, last ] + try: + tracksframes = map(lambda x: x[0] * 60 * 75 + x[1] * 75 + x[2], tracks) + ret.extend(tracksframes[:-1]) + ret.append(tracksms[-1]) + except IndexError: + tracksframes = map(lambda x: x[0] * 60 * 75 + x[1] * 75, tracks) + ret.extend(tracksframes[:-1]) + ret.append(tracksms[-1]) + return ret + +if __name__ == '__main__': + tracks = [ (0, 3, 71), (5, 44, 0) ] + tracksdiskinfo = [ 0x03015501, 1, 296, 344 ] + diskinfo = discid(tracks) + + assert diskinfo == tracksdiskinfo + + print '%08x' % diskinfo[0], + print ' '.join(map(str, diskinfo[1:])) diff --git a/pymeds.py b/pymeds.py index 1ec786c..1702d72 100755 --- a/pymeds.py +++ b/pymeds.py @@ -29,9 +29,11 @@ def tryloadmodule(mod): # mpegtsmod can be really expensive. modules = [ 'shoutcast', + 'audio', 'Clip', 'pyvr', 'item', + 'slinkemod', 'dvd', 'ZipStorage', 'mpegtsmod', diff --git a/slinke.py b/slinke.py new file mode 100644 index 0000000..d7eff01 --- /dev/null +++ b/slinke.py @@ -0,0 +1,1085 @@ +#!/usr/bin/env python + +from twisted.internet.protocol import BaseProtocol, Protocol, ConnectedDatagramProtocol +from twisted.internet.serialport import SerialPort +from twisted.internet import defer, reactor, threads +from zope.interface import implements, Interface, Attribute + +import array +import cddb +import serial +import sys +import time + +class ICDPlayerTransport(Interface): + power = Attribute('A C{bool}') + playing = Attribute('A C{bool}') + paused = Attribute('A C{bool}') + dooropen = Attribute('A C{bool}') + discknown = Attribute('A C{bool}') + curdisc = Attribute('A C{int} of the current disc loaded or None.') + alldiscs = Attribute('A C{bool}, does playback include all discs.') + repeateall = Attribute('A C{bool}') + repeateone = Attribute('A C{bool}') + shuffle = Attribute('A C{bool}') + program = Attribute('False or 1, 2 or 3') + cdnum = Attribute('A number that is the CD player ID') + disccount = Attribute('A count of the capacity of the player.') + model = Attribute('The model.') + + def play(): + '''Start playing at current possition.''' + + def stop(): + '''Stop playing.''' + + def pause(): + '''Pause.''' + + def togglepause(): + '''Toggle Paused state. Will trigger the respective paused + or playing on the protocol.''' + + def nexttrack(): + '''Next Track.''' + + def previoustrack(): + '''Previous Track.''' + + def poweron(): + '''Turn power on. Returns a defered that will be triggered + when power is on.''' + + def poweroff(): + '''Turn power off.''' + + def discinfo(d): + '''Report disc info, will stop playback. Returns Deferred.''' + + def trackinfo(d, t): + '''Report track info, will stop playback. Returns Deferred.''' + + def playdisc(disc, track=1): + '''Start playing disc. Returns Deferred.''' + + def cuedisc(disc, track=1): + '''Cue disc to track. Returns Deferred.''' + + def loaddisc(disc, track=1, load=True): + '''Make sure disc is loaded, cue if necessary. Returns a + Deferred. If load is False, will not load the disc. The + Deferred will return True if the disc is loaded, otherwise + False.''' + + def discs(): + '''Return set of discs present, or None if the discs are + unknown at this time.''' + + def __contains__(k): + '''Check if a disc is present in the changer.''' + + def __len__(k): + '''Number of discs present in the changer. This is not the + capacitity..''' + + def __iter__(): + '''Iterate over all the discs in the changer.''' + + def setcontinuous(alldiscs=True): + '''Set continuous playback, if alldiscs is False, one disc.''' + + def setshuffle(alldiscs=True): + '''Set shuffle playback, if alldiscs is False, one disc.''' + + def settimeout(timeout=300): + '''After timeout seconds of no commands being issues, and + the player not playing discs, power off.''' + + def getcddbid(disc): + '''Return CDDB id of disc.''' + +class CDPlayerProtocol(BaseProtocol): + '''This protocol is a bit different in that the transport implements + the CDPlayerTransport interface.''' + + def stateChanged(self): + # one of the attributes changes + pass + +class SLinkException(Exception): + pass + +class DiscError(SLinkException): + def __init__(self, disc, s): + SLinkException.__init__(self, s % disc) + self.disc = disc + +class NoDisc(SLinkException): + pass + +# Not really BaseProtocol +class SLinkCDTransport(object): + implements(ICDPlayerTransport) + + def __init__(self, sprotocol, cdnum): + if cdnum not in (1, 2, 3): + raise ValueError('invalid CD: %d' % cdnum) + self._proto = sprotocol + self._cdnum = cdnum + self._childprotocol = None + + # Internal state variables + self._discs = None + self._discpos = None + self._disccount = None + + self.loadeddisc = None + self._responses = {} + + def makeConnection(self, protocol): + print 'mC' + self._childprotocol = protocol + # We can now start issuing commands to _proto, our parent. + # Once we have gathered all the state we need, we will + # connect to our child protocol. + l = [] + d = self.runCommand('queryplayercap', 'decksize') + d.addCallback(lambda x: self.printargs('c')) + l.append(d) + + d = self.runCommand('querydeckmodel', 'deckmodel') + d.addCallback(self.processdeckmodel) + d.addCallback(lambda x: self.printargs('b')) + l.append(d) + + d = self.runCommand('querystatus', 'status') + d.addCallback(lambda x: self.printargs('after querystatus done')) + l.append(d) + + d = defer.DeferredList(l) + d.addCallback(lambda x: self.printargs('here')) + d.addCallback(lambda x, p=protocol, s=self: p.makeConnection(s)) + + def printargs(self, args): + print 'mpa:', `args` + + def processdeckmodel(self, args): + self._model = args[0] + + def processstatus(self, args): + a = array.array('B', args[0]) + f = a[0] + r = None + if f & 0x10: + self._power = False + else: + self._power = True + + if f & 0x2f == 0x2f: + self._dooropen = True + self._playing = False + self._paused = False + else: + self._dooropen = False + if f & 0x1: + self._playing = True + else: + self._playing = False + if f & 0x2: + self._paused = True + else: + self._paused = False + + f = a[1] + if f & 0x80: + self._discknown = True + if self._discs is None: + r = self.getdiscs() + else: + self._discknown = False + #self._discs = None + + if self._power is True: + # We don't know discs yet, try again in a few + # seconds. + r = defer.Deferred() + reactor.callLater(1, lambda: + self.runCommand('querystatus', + 'status').chainDeferred(r)) + + if f & 0x40: + self._alldiscs = True + else: + self._alldiscs = False + + if f & 0x10: + self._repeateall = True + self._repeateone = False + elif f & 0x20: + self._repeateall = False + self._repeateone = True + else: + self._repeateall = False + self._repeateone = False + + if f & 0xf == 0x1: + self._shuffle = True + self._program = False + elif f & 0xf in (0x4, 0x5, 0x6): + self._shuffle = False + self._program = (f & 0xf) - 0x3 + else: + self._shuffle = False + self._program = False + + print 'self:', `self`, `r` + return r + + def __repr__(self): + return '' % ', '.join('%s=%s' % (k, `getattr(self, k)`) for k in ( 'power', 'playing', 'paused', 'dooropen', 'discknown', 'alldiscs', 'repeateall', 'repeateone', 'shuffle', 'program', 'disccount', 'model', )) + + def getdiscs(self): + print 'gd called' + self._discsnew = set() + l = self.runCommand('querydiscs', [ 'disc_list%d' % i for i in xrange((self.disccount + 103) / 104) ]) + for i, d in enumerate(l): + d.addCallback(lambda x, i=i: self.processdisclist(i, x)) + + r = defer.DeferredList(l) + r.addCallback(self.freezediscs) + return r + + def freezediscs(self, args): + print 'fd called' + self._discs = frozenset(self._discsnew) + self._discsnew = None + + bits = dict((1 << i, i) for i in xrange(8)) + + def processdisclist(self, part, args): + off = part + a = array.array('B', args[0]) + for pos, i in enumerate(a): + while i: + b = i & -i + bitpos = self.bits[b] + i ^= b + self._discsnew.add(104 * off + pos * 8 + + bitpos + 1) + + def insertresponse(self, resp, obj): + if resp in self._responses: + self._responses[resp].append(obj) + else: + self._responses[resp] = [ obj ] + + def runCommand(self, cmd, response, *args, **kwargs): + for i in kwargs.pop('ignoreresponses', []): + d = defer.Deferred() + self.insertresponse(i, d) + + if isinstance(response, list): + d = [] + for i in response: + dd = defer.Deferred() + self.insertresponse(i, dd) + d.append(dd) + else: + d = defer.Deferred() + self.insertresponse(response, d) + + self._proto.sendmsg(cmd, self._cdnum, *args) + return d + + def gotResponse(self, cmd, *args): + # Got a response from a command + expected = True + + print 'gR:', `cmd`, `args` + # A few commands are state change, so handle them here. + r = None + if cmd in ('nowat', 'tocread', 'trackinfo', 'track_info', 'discinfo', ): + self._discpos = args[0] + elif cmd == 'poweron': + print 'poweron, doing querystatus' + r = self.runCommand('querystatus', 'status') + elif cmd == 'poweroff': + self._discpos = None + r = self.runCommand('querystatus', 'status') + elif cmd == 'decksize': + if self._disccount is not None and self._disccount != \ + args[0]: + # XXX state changed! + pass + self._disccount = args[0] + elif cmd == 'status': + r = self.processstatus(args) + #elif cmd == 0x18 + # self._discs = None + else: + expected = False + + if r is not None: + r.addCallback(lambda x: self.finishResponse(cmd, + expected, *args)) + return + + self.finishResponse(cmd, expected, *args) + + def finishResponse(self, cmd, expected, *args): + if cmd not in self._responses: + if not expected: + print 'Unexpected response:', `cmd`, `args` + return + + # We pop so that a future command will be scheduled properly + for i in self._responses.pop(cmd): + i.callback(args) + + power = property(lambda x: x._power) + playing = property(lambda x: x._playing) + paused = property(lambda x: x._paused) + dooropen = property(lambda x: x._dooropen) + discknown = property(lambda x: x._discknown) + curdisc = property(lambda x: x._discpos) + alldiscs = property(lambda x: x._alldiscs) + repeateall = property(lambda x: x._repeateall) + repeateone = property(lambda x: x._repeateone) + shuffle = property(lambda x: x._shuffle) + program = property(lambda x: x._program) + cdnum = property(lambda x: x._cdnum) + disccount = property(lambda x: x._disccount) + model = property(lambda x: x._model) + + def play(self): + '''Start playing at current possition.''' + return self.runCommand('play', 'play') + + def stop(self): + '''Stop playing.''' + return self.runCommand('stop', 'stop') + + def pause(self): + '''Pause.''' + return self.runCommand('pause', 'pause') + + def togglepause(self): + '''Toggle Paused state. Will trigger the respective paused + or playing on the protocol.''' + l = self.runCommand('togglepause', [ 'play', 'pause', ]) + return defer.DeferList(l, fireOnOneCallback=True, + fireOnOneErrback=True) + + def nexttrack(self): + '''Next Track.''' + raise NotImplementedError + + def previoustrack(self): + '''Previous Track.''' + raise NotImplementedError + + def poweron(self): + '''Turn power on.''' + if not self.power: + return self.runCommand('poweron', 'poweron') + + return defer.succeed(()) + + def poweroff(self): + '''Turn power off.''' + if self.power: + return self.runCommand('poweroff', 'poweroff', + ignoreresponses=[ 'stop', ]) + + return defer.succeed(()) + + def discinfo(self, disc): + '''Report disc info, will stop playback. Returns Deferred.''' + print 'di:, scheduling loaddisc' + return self.loaddisc(disc) \ + .addCallback(lambda x: + self.runCommand('querydiscinfo', 'discinfo', disc)) + + def trackinfo(self, disc, track): + '''Report track info, will stop playback. Returns Deferred.''' + print 'ti:, scheduling loaddisc' + return self.loaddisc(disc).addCallback(lambda x: + self.runCommand('querytrackinfo', 'track_length', disc, + track)) + + def playdisc(self, disc, track=1): + '''Start playing disc. Returns Deferred.''' + return self.loaddisc(disc, track).addCallback(lambda x: + self.runCommand('playtrack', 'track_info', disc, track)) + + def cuedisc(self, disc, track=1, noload=False): + '''Cue disc to track. Returns Deferred.''' + # XXX - should we wait for the pause response? + # XXX - we'll recurse, but that should be fine + if noload: + print 'z' + d = defer.succeed(None) + else: + print 'w' + d = self.loaddisc(disc, track) + # XXX - second cue doesn't return track_info or something + d.addCallback(lambda x: self.stop()) + return d.addCallback(lambda x: + self.runCommand('cuetrack', 'track_info', disc, track, + ignoreresponses=[ 'pause', ])) + + def loaddisc(self, disc, track=1, load=True): + '''Make sure disc is loaded, cue if necessary. Returns Deferred.''' + if self._discpos != disc and load: + print 'x', `self._discpos`, `disc` + d = self.poweron() + d.addCallback(lambda x: self.setcontinuous()) + d.addCallback(lambda x: self.cuedisc(disc, noload=True)) + else: + print 'y' + d = defer.succeed(None) + + d.addCallback(lambda x: self._discpos == disc) + return d + + def discs(self): + '''Return set of discs present.''' + return self._discs + + def __contains__(self, k): + '''Check if a disc is present in the changer.''' + return k in self._discs + + def __len__(self): + '''Number of discs present in the changer. This is not the + capacitity..''' + return len(self._discs) + + def __iter__(self): + '''Iterate over all the discs in the changer.''' + + # We use this so that when the discs available changes we + # automaticly update this. + for i in xrange(self.disccount): + if i in self: + yield i + + def setmode(self, fun): + print 'sm:', `fun` + if fun(): + print 'passed!', `self` + return + + d = defer.Deferred() + self.insertresponse('status', d) + d.addCallback(lambda x: self.setmode(fun)) + # send IR code + print 'sending ir' + self._proto.sendir('\xeb\x91') + return d + + def setcontinuous(self, alldiscs=True): + '''Set continuous playback, if alldiscs is False, one disc.''' + fun = lambda: not self.program and not self.shuffle and \ + self.alldiscs == alldiscs + if not fun() and (self.playing or self.paused): + d = self.stop() + else: + d = defer.succeed(None) + + return d.addCallback(lambda x: self.setmode(fun)) + + def setshuffle(self, alldiscs=True): + '''Set shuffle playback, if alldiscs is False, one disc.''' + if self.playing or self.paused: + d = self.stop() + raise NotImplementedError + + def settimeout(self, timeout=300): + '''After timeout seconds of no commands being issues, and + the player not playing discs, power off.''' + raise NotImplementedError + + # Imported from eslink.py, w/ minor changes + @defer.deferredGenerator + def getcddbid(self, disc): + '''Return CDDB id of disc.''' + + r = defer.waitForDeferred(self.discinfo(disc)) + yield r + r = r.getResult() + if r[0] != disc: + # if the disc isn't present, update the discs + print 'Disc missing, updating discs' + r = defer.waitForDeferred(self.getdiscs()) + yield r + r = r.getResult() + raise NoDisc(disc, 'disc %d not present') + + r = r[1:] + def addreducems(i, a): + ni = (i[0] + a[0], i[1] + a[1]) + m, s = divmod(ni[1], 60) + i = list(i) + i[0] = ni[0] + m + i[1] = s + return tuple(i) + + tracks = [] + curtot = (0, 2) + tracklens = [] + for i in range(1, r[0] + 1): + tracks.append(curtot) + j = defer.waitForDeferred(self.trackinfo(disc, i)) + yield j + j = j.getResult() + assert j[0] == disc + assert j[1] == i + j = j[2:] + tracklens.append(j) + curtot = addreducems(curtot, tracklens[-1]) + + tracks.append(r[1:]) + tracks[-1] = addreducems(tracks[-1], (0, 2)) + id = cddb.discid(tracks) + + yield id + +def nulltrim(buf): + s = buf.find('\x00') + if s == -1: + return buf + + return buf[:s] + +class SLinkProtocol(ConnectedDatagramProtocol): + '''Interface for an S-Link Port. To be passed into something that + passes messages to/from an S-Link Port.''' + + simpcmds = { + 'play': '\x00', + 'stop': '\x01', + 'pause': '\x02', + 'togglepause': '\x03', + 'nexttrack': '\x08', + 'queryplayercap': '\x22', + 'poweron': '\x2e', + 'poweroff': '\x2f', + } + _bysimpcmds = dict((v, k) for k, v in simpcmds.iteritems()) + #_bysimpcmds['\x0e'] = 'poweron' # Disc loaded? + + simpcmds['querystatus'] = '\x0f' + simpcmds['querydiscinfo'] = '\x44' + simpcmds['querytrackinfo'] = '\x45' + simpcmds['playtrack'] = '\x50' + simpcmds['cuetrack'] = '\x51' + simpcmds['querydeckmodel'] = '\x6a' + simpcmds['querydiscs'] = '\x72' + + # Bytes returned by cmd 0x61 + decksizes = { + '\x05\x63': 5, + '\xfe\x0b': 200, + '\x64\x6b': 300, + '\x64\x0b': 300, + '\xfe\x6b': 300, + '\xc8\x6b': 400, + } + + def discdecode(hidisc, x): + disc = array.array('B', x)[0] + if hidisc: + return disc + 200 + elif disc == 0x00: + return 100 # XXX? + elif disc >= 0x9a: + return 100 + disc - 0x9a + else: + return bcdtoint(disc) + + def discinfo(hidisc, x, dd=discdecode): + a = array.array('B', x) + return dd(hidisc, x), bcdtoint(a[2]), \ + bcdtoint(a[3]), bcdtoint(a[4]), bcdtoint(a[5]) + + def trackinfo(hidisc, x, dd=discdecode): + a = array.array('B', x) + return dd(hidisc, x), bcdtoint(a[1]), \ + bcdtoint(a[2]), bcdtoint(a[3]) + + responses = { + '\x0e': lambda hidisc, x: ('invalidmode', ()), + '\x14': lambda hidisc, x: ('discinfo', (None, )), + '\x15': lambda hidisc, x: ('trackinfo', (None, )), + '\x45': lambda hidisc, x: ('trackinfo', (x, )), + '\x50': lambda hidisc, x, ti=trackinfo: ('track_info', + ti(hidisc, x)), + '\x52': lambda hidisc, x, dd=discdecode: ('tocread', + (dd(hidisc, x),)), + '\x58': lambda hidisc, x, dd=discdecode: ('nowat', + (dd(hidisc, x),)), + '\x60': lambda hidisc, x, di=discinfo: ('discinfo', + di(hidisc, x)), + '\x61': lambda hidisc, x, y=decksizes: ('decksize', + (y[x],)), + '\x62': lambda hidisc, x, ti=trackinfo: ('track_length', + ti(hidisc, x)), + '\x6a': lambda hidisc, x: ('deckmodel', + (nulltrim(x).decode('ascii'),)), + '\x70': lambda hidisc, x: ('status', (x, )), + '\x72': lambda hidisc, x: ('disc_list0', (x, )), + '\x73': lambda hidisc, x: ('disc_list1', (x, )), + '\x74': lambda hidisc, x: ('disc_list2', (x, )), + '\x75': lambda hidisc, x: ('disc_list3', (x, )), + } + + def __init__(self): + self._queue = [] + self._cdplayers = {} + self._pendingmsgs = {} + self._started = False + # Should be set by class + + def startProtocol(self): + self._started = True + for fun, arg in self._queue: + fun(arg) + self._queue = None + + def stopProtocol(self): + self._started = False + + def connectCDPlayer(self, num, protocol): + if num < 1 or num > 3: + raise ValueError('invalid CD number: %d' % num) + + if not isinstance(protocol, CDPlayerProtocol): + raise ValueError('protocol must be CDPlayerProtocol') + + if num in self._cdplayers: + raise ValueError('CD player already attached') + + transport = SLinkCDTransport(self, num) + if not self._started: + self._queue.append((transport.makeConnection, + protocol)) + else: + transport.makeConnection(protocol) + + self._cdplayers[num] = transport + + def datagramReceived(self, msg, port): + '''Decode message and dispatch (if possible).''' + + mar = array.array('B', msg) + + if (mar[0] & 0xf0) != 0x90: + print 'unknown msg:', msg.encode('hex') + + if msg in self._pendingmsgs: + self._pendingmsgs[msg].cancel() + del self._pendingmsgs[msg] + return + + if (mar[0] & 0x8) != 0x8: + # command from another device, ignore + #print 'unkn command:', msg.encode('hex') + return + + # From here all msgs are responses + cdnum = (mar[0] & 0x3) + 1 + hidisc = False + if cdnum > 3: + hidisc = True + cdnum -= 3 + + if cdnum not in self._cdplayers: + # We don't have this CD Player registered. + print 'unkn responses %d:' % cdnum, msg.encode('hex') + return + + if len(msg) == 2 and msg[1] in self._bysimpcmds: + attr = self._bysimpcmds[msg[1]] + args = () + elif msg[1] in self.responses: + attr, args = self.responses[msg[1]](hidisc, msg[2:]) + else: + print 'foo:', msg.encode('hex') + return + try: + self._cdplayers[cdnum].gotResponse(attr, *args) + except TypeError, e: + if str(e) == 'gotResponse() argument after * must be a sequence': + raise TypeError('args for %s not sequence' % `msg[1].encode('hex')`) + else: + print 'te:', `str(e)` + raise + + # Imported from eslink.py + def makeaddrcmd(self, msgto, cmd, cdnum, disc=None, track=None, + hibit=False): + addr = 0x90 + (cdnum - 1) + if not msgto: + addr |= 0x8 + + if disc != None: + if disc > 200: + addr += 3 + disc -= 200 + elif disc < 100: + disc = inttobcd(disc) + else: + disc = 0x9a + disc - 100 + if track is None: + return '%c%c%c' % (addr, cmd, disc) + else: + track = inttobcd(track) + return '%c%c%c%c' % (addr, cmd, disc, track) + else: + if hibit: + addr += 3 + return '%c%c' % (addr, cmd) + + def schedulemsg(self, msg): + print 'sending msg:', `msg` + self.transport.write(msg) + # XXX - should probably recall sendmsg to reschedule + if msg in self._pendingmsgs: + print 'old:', self._pendingmsgs[msg] + self._pendingmsgs[msg] = reactor.callLater(1, + lambda m=msg: self.schedulemsg(msg)) + + def sendir(self, code): + # XXX make it properly addressable + self.transport.write(code) + + def sendmsg(self, type, cdpnum, disc=None, track=None): + msg = self.makeaddrcmd(True, self.simpcmds[type], cdpnum, + disc, track) + self.schedulemsg(msg) + +def bcdtoints(i): + t, o = divmod (i, 16) + assert t < 10 and o < 10 + + return t, o + +def inttobcd(i): + assert i < 100, 'i is invalid: %s' % `i` + t, o = divmod(i, 10) + + return t << 4 | o + +def bcdtoint(i): + t, o = divmod (i, 16) + assert t < 10 and o < 10 + + return t * 10 + o + +class SLinkEPort(object): + '''Implement one port transport for SLink-E.''' + + def __init__(self, slinke, port, protocol): + self.slinke = slinke + self.port = port + self.protocol = protocol + self.openned = True + + def write(self, msg): + if not self.openned: + raise RuntimeError('port has been closed') + + self.slinke.sendmsg(self.port, [ msg ]) + + def writeSequence(self, data): + self.slinke.sendmsg(self.port, data) + + def loseConnection(self): + '''Close the port. Will return a Deferred object that is + called when the port has finished closing.''' + + self.openned = False + self.slinke.closePort(self.port) + + def getPeer(self): + return self.port + +class SLinkE(Protocol): + '''Interface for talking to the SLink-E. It sends commands, and + dispatched commands recevied for the port.''' + + commands = { + 'reset': '\xff\xff', + 'getversion': '\xff\x0b', + 'getserial': '\xff\x0c', + 'disableport': lambda x: chr((x << 5) | 0x1f) + '\x02', + 'enableport': lambda x: chr((x << 5) | 0x1f) + '\x03', + } + + # XXX - It's assumed all responses are two bytes. + receivedresp = { + 'versionis': ('\xff\x0b', 1), + 'portenabled': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and + x[1] == '\x03', 0), + 'portdisabled': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and + x[1] == '\x02', 0), + 'slinkerror': (lambda x: (ord(x[0]) & 0x1f) == 0x1f and + x[1] == '\x80', 0), + } + + version = property(lambda x: x._version) + #serial = property(lambda x: x._serial) + + def __init__(self, *args, **kwargs): + self._ports = {} + self._portqueue = {} + self._portpending = {} + self._buffer = [] + self._bufferlen = 0 + self._currentresp = None + self._bytesneeded = None + _version = (None, None) + + def sendcmd(self, cmd): + #print 'sending:', `cmd` + self.transport.write(cmd) + + def openPort(self, port, protocol): + if port in self._ports: + raise ValueError('port already opened') + if port in self._portpending: + raise ValueError('port already pending') + + self.sendcmd(self.commands['enableport'](port)) + self._portpending[port] = SLinkEPort(self, port, protocol) + + def closePort(self, port): + self.sendcmd(self.commands['disableport'](port)) + + def sendmsg(self, port, msg): + '''Send the sequence of strings in msg out the port.''' + pos = 0 + fragpos = 0 + msgbuf = [] + while pos != len(msg) and fragpos != len(msg[-1]): + fraglen = 0 + msgfrag = [] + while fraglen < 30 and pos != len(msg) and \ + fragpos != len(msg[-1]): + msgfrag.append(msg[pos][fragpos:fragpos + (30 - fraglen)]) + fragpos += len(msgfrag[-1]) + fraglen += len(msgfrag[-1]) + if fragpos == len(msg[pos]): + pos += 1 + fragpos = 0 + + #print 'h:', `fraglen`, `msgfrag` + msgbuf.append(chr((port << 5) | fraglen)) + msgbuf.extend(msgfrag) + + # Port Send End + msgbuf.append(chr(port << 5)) + + #print 'sending:', `msgbuf` + self.transport.writeSequence(msgbuf) + + # Protocol Methods + def connectionMade(self): + Protocol.connectionMade(self) + #self.sendcmd(self.commands['reset']) + #time.sleep(1) + self.sendcmd(self.commands['getversion']) + #self.sendcmd(self.commands['disableport'](7)) + #for i in xrange(6): + # self.sendcmd(self.commands['enableport'](i)) + + def dataReceived(self, data): + #print 'dR:', `data` + self._buffer.append(data) + self._bufferlen += len(data) + i = True + while i and self._buffer: + i = self.checkBuffer() + + # Internal Buffer routines + def peakBuffer(self, cnt): + if cnt > self._bufferlen: + return + + r = [] + rem = cnt + for buf in self._buffer: + r.append(buf[:rem]) + rem -= len(r[-1]) + if rem == 0: + break + + r = ''.join(r) + assert len(r) == cnt, 'failed: cnt=%d, buffer=%s' % (cnt, self._buffer) + return r + + def consumeBuffer(self, cnt): + assert self._bufferlen >= cnt + + rem = cnt + while rem: + buf = self._buffer.pop(0) + self._bufferlen -= len(buf) + if len(buf) > rem: + self._buffer.insert(0, buf[rem:]) + self._bufferlen += len(buf) - rem + return + + rem -= len(buf) + + def getBuffer(self, cnt): + buf = self.peakBuffer(cnt) + if buf is None: + return + + self.consumeBuffer(cnt) + return buf + + def lenBuffer(self): + return self._bufferlen + + def checkBuffer(self): + '''Dispatch messages if available in buffer. Returns True if + a message was dispatched. Returns False if we need more data + to dispatch.''' + + if self._currentresp is not None: + raise NotImplementedError + + firstbyte = ord(self._buffer[0][0]) + portnum = firstbyte >> 5 + bytecnt = (firstbyte & 0x1f) + #print 'portnum:', portnum, 'bytecnt:', bytecnt, 'buf:', self._buffer + if bytecnt == 0: + # port receive end + self.consumeBuffer(1) + if portnum not in self._portqueue: + # XXX - stale data + return + msg = ''.join(self._portqueue[portnum]) + self._portqueue[portnum] = [] + #print 'delivering msg %d:' % portnum, `msg` + self._ports[portnum].protocol.datagramReceived(msg, portnum) + elif bytecnt == 0x1f: + # modify command + buf = self.peakBuffer(2) + if buf is None: + return False + for k, (resp, extracnt) in self.receivedresp.iteritems(): + if (callable(resp) and resp(buf)) or buf == resp: + if self.lenBuffer() < len(buf) + extracnt: + return False + self.consumeBuffer(len(buf)) + extra = self.getBuffer(extracnt) + self.dispatch(k, portnum, extra) + return True + else: + raise RuntimeError('unhandled msg: %s' % `''.join(self._buffer)`) + else: + # port receive data + buf = self.peakBuffer(bytecnt + 1) + if buf is None: + return False + + #print 'f:', bytecnt, `buf` + self.consumeBuffer(bytecnt + 1) + if portnum in self._portqueue: + self._portqueue[portnum].append(buf[1:]) + + return True + + def dispatch(self, k, portnum, extra): + '''Dispatch a message.''' + #print 'dispatching: %s(%d, %s)' % (`k`, portnum, `extra`) + getattr(self, 'R_%s' % k)(portnum, extra) + + # Messages + def R_versionis(self, portnum, data): + assert len(data) == 1 + self._version = bcdtoints(ord(data[0])) + #print 'version:', `self._version` + + def R_portdisabled(self, portnum, data): + if portnum == 7: + assert not self._ports, \ + 'disabled, but ports present: %s' % `self._ports` + return + + pobj = self._ports[portnum] + + del self._ports[portnum] + del self._portqueue[portnum] + + # Signal that we are done + pobj.protocol.doStop() + + def R_portenabled(self, portnum, data): + if portnum not in self._portpending: + #print 'skipping:', portnum + return + + self._ports[portnum] = self._portpending[portnum] + self._portqueue[portnum] = [] + del self._portpending[portnum] + self._ports[portnum].protocol.makeConnection(self._ports[portnum]) + + def R_slinkerror(self, portnum, data): + pass + #print 'got slinkerror on port:', `portnum` + +# Test Code + +class MyCDPlayer(CDPlayerProtocol): + @staticmethod + def printarg(args): + print 'pa:', `args` + + def connectionMade(self): + print 'power:', `self.transport.power` + if not self.transport.power: + print 'pon:', `self.transport.poweron()` + print self.transport.discs() + self.transport.trackinfo(32, 5).addCallback( + self.printarg).addCallback(lambda x: self.transport.discinfo(32)).addCallback(self.printarg).addCallback(lambda x: self.transport.poweroff()).addCallback(lambda x: reactor.stop()) + #reactor.callLater(20, self.transport.poweroff) + + #print 'powered on, playing...' + #self.transport.querydeckmodel() + #self.transport.querystatus() + #self.transport.querydiscinfo(5) + #self.transport.querytrackinfo(5, 3) + #self.transport.playtrack(10, 3) + + #print 'powered off...' + #reactor.stop() + + def stateChanged(self): + print 'sC' + +if __name__ == '__main__': + if False: + import serial + s = serial.Serial('/dev/ttyU0', baudrate=38400, rtscts=True, timeout=5) + s.write('\xff\xff') + print 'reset' + time.sleep(1) + + s.write(''.join(['\x02', '\x90"', '\x00'])) + print 'cmd' + while True: + print 'resp:', `s.read(1)` + sys.exit(0) + + slinke = SLinkE() + s = SerialPort(slinke, '/dev/ttyU0', reactor, baudrate=38400, rtscts=True) + th = SLinkProtocol() + slinke.openPort(0, th) + th.connectCDPlayer(1, MyCDPlayer()) + #reactor.callLater(5, lambda: slinke.closePort(0)) + reactor.run() diff --git a/slinkemod.py b/slinkemod.py new file mode 100644 index 0000000..890e953 --- /dev/null +++ b/slinkemod.py @@ -0,0 +1,559 @@ +#!/usr/bin/env python +# Copyright 2009 John-Mark Gurney +'''CD Changer Module''' + +__version__ = '$Change: 1366 $' +# $Id: //depot/python/pymeds/main/shoutcast.py#24 $ + +import CDDB +import audio +import filelock +import os.path +import shelve +import slinke + +from DIDLLite import Container, StorageSystem, StorageVolume +from DIDLLite import MusicGenre, MusicTrack, MusicAlbum, Resource, ResourceList +from FSStorage import registerklassfun + +from twisted.python import log, threadable, failure +from twisted.internet import defer, protocol, reactor, threads +from twisted.internet.interfaces import IPushProducer, IPullProducer, IConsumer +from twisted.internet.serialport import SerialPort +from twisted.web import error, http, resource, server + +from zope.interface import implements + +def isint(s): + try: + i = int(s) + return True + except ValueError: + return False + +class LimitedAudioProducer(object): + implements(IPushProducer, IPullProducer, IConsumer) + + # request, args + def __init__(self, consumer, *args, **kwargs): + self.remain = self.setlength(consumer) + self.consumer = consumer + audio.AudioPlayer(self, *args, **kwargs) + + @staticmethod + def setlength(request): + if request.responseHeaders.hasHeader('content-length'): + secs = int( + request.responseHeaders.getRawHeaders( + 'content-length')[0].strip()) + r = secs * audio.bytespersecmt( + request.responseHeaders.getRawHeaders( + 'content-type')[0]) + request.responseHeaders.setRawHeaders( + 'content-length', [ str(r) ]) + else: + r = None + + return r + + def shutdown(self): + print 'lap: shutdown', `self.consumer` + # XXX - I still get writes after I've asked my producer to stop + self.write = lambda x: None + self.producer.stopProducing() + self.producer = None + self.consumer.unregisterProducer() + self.consumer = None + #self.remain = 0 # No more writes + + # IPushProducer + def pauseProducing(self): + return self.producer.pauseProducing() + + def resumeProducing(self): + return self.producer.resumeProducing() + + def stopProducing(self): + print 'lap: sp' + self.shutdown() + + # IConsumer + def registerProducer(self, producer, streaming): + print 'lap: regp' + self.producer = producer + self.streamingProducer = streaming + + self.consumer.registerProducer(self, streaming) + + def unregisterProducer(): + print 'lap: unregp' + self.shutdown() + + def write(self, data): + if self.remain is not None: + data = data[:self.remain] + self.remain -= len(data) + self.consumer.write(data) + + if self.remain is not None and not self.remain: + # Done producing + self.shutdown() + +class LimitedAudioRes(audio.AudioResource): + producerFactory = LimitedAudioProducer + + def render(self, request): + r = audio.AudioResource.render(self, request) + + if request.method == 'HEAD': + LimitedAudioProducer.setlength(request) + + return r + +class ChangerTrack(resource.Resource): + isLeaf = True + + def __init__(self, obj, res): + resource.Resource.__init__(self) + self._obj = obj + self._res = res + + def getmimetype(self): + return self._res.getmimetype() + + def render(self, request): + print 'CTrender:', `request.postpath`, `request.method`, `request` + self.setlength(request) + + if request.method == 'HEAD': + return self._res.render(request) + + # Needs to be created here so that it will fire. + # If not, the list may be walked before we add it. + nf = request.notifyFinish() + + d = self._obj.trylock(20, request) + d.addCallback(lambda x: self._obj.cue()) + # XXX - how to stop playback when track is done? + #d.addCallback(lambda x: self.setlength(request)) + d.addCallback(lambda x: self.printarg('before render, after cue')) + + d.addCallback(lambda x: self._res.render(request)) + + d.addCallback(lambda x: self.docleanup(nf, request)) + + d.addErrback(lambda exc: self.failed(exc, request)) + # XXX - add errBack? + d.addCallback(lambda x: self.printarg('after render, before play')) + d.addCallback(lambda x: self._obj.play()) + d.addErrback(self.logerr) + + return server.NOT_DONE_YET + + def setlength(self, request): + r = self._obj.getlength() + request.responseHeaders.setRawHeaders( + 'content-length', [ str(r) ]) + + def docleanup(self, nf, request): + print 'docleanup' + + nf.addBoth(self.dostop) + nf.addBoth(lambda x: self.dounlock(request)) + nf.addErrback(self.logerr, 'nf') + + def dounlock(self, request): + self._obj.unlock(request) + # Cancel the error back so we don't get a warning. + return None + + def dostop(self, arg): + print 'dostop' + d = self._obj.stop() + # Make sure we have stopped before we continue + d.addBoth(lambda x: arg) + return d + + def logerr(self, exc, *args): + print 'logerr:', `args` + exc.printTraceback() + #exc.printDetailedTraceback() + + def failed(self, exc, request): + print 'in this failed case', self._obj.haslock(request), `request` + if self._obj.haslock(request): + self.dounlock(request) + # XXX - look at queue and decide + #request.responseHeaders.addRawHeader('Retry-After') + res = error.ErrorPage(http.SERVICE_UNAVAILABLE, + 'failed w/ Exception', exc).render(request) + request.write(res) + request.finish() + return exc + + def printarg(self, args): + print 'pa:', `self`, `args` + + def unregisterProducer(self): + resource.Resource.unregisterProducer(self) + raise NotImplementedError + +class SLinkEChangerDiscTrack(MusicTrack): + def __init__(self, *args, **kwargs): + discobj = kwargs.pop('discobj') + track = kwargs.pop('track') + kwargs['content'] = ChangerTrack(self, discobj.getResource()) + MusicTrack.__init__(self, *args, **kwargs) + + self._discobj = discobj + self._track = track + self.originalTrackNumber = str(track) + + if False: + self.bitrate = bitrate + + self.url = '%s/%s' % (self.cd.urlbase, self.id) + self.res = ResourceList() + res = Resource(self.url, 'http-get:*:%s:*' % + kwargs['content'].getmimetype()) + l = self.getlength() + if l is not None: + res.duration = l + self.res.append(res) + + def getlength(self): + '''Returns None if length in seconds is unavailable.''' + track = self._track + cddbid = self._discobj.getID()[2:] + cddbid[-1] = cddbid[-1] * 75 # frames/sec + return (cddbid[track] - cddbid[track - 1]) // 75 + + def trylock(self, t, obj): + return self._discobj.trylock(t, obj) + + def haslock(self, obj): + return self._discobj.haslock(obj) + + def unlock(self, obj): + self._discobj.unlock(obj) + + def cue(self): + return self._discobj.cueTrack(self._track) + + def play(self): + return self._discobj.play() + + def stop(self): + return self._discobj.stop() + +class SLinkEChangerDisc(MusicAlbum): + def __init__(self, *args, **kwargs): + changer = kwargs.pop('changer') + self._changer = changer + disc = kwargs.pop('disc') + self._disc = disc + self._discnum = int(disc) + self._lastid = None + kwargs['content'] = ChangerTrack(self, self.getResource()) + MusicAlbum.__init__(self, *args, **kwargs) + + self.url = '%s/%s' % (self.cd.urlbase, self.id) + self.res = ResourceList() + res = Resource(self.url, 'http-get:*:%s:*' % + kwargs['content'].getmimetype()) + l = self.getlength() + if l is not None: + res.duration = l + self.res.append(res) + + def getlength(self): + return self.getID()[-1] + + def trylock(self, t, obj): + return self._changer.trylock(t, obj) + + def haslock(self, obj): + return self._changer.haslock(obj) + + def unlock(self, obj): + return self._changer.unlock(obj) + + def getResource(self): + return self._changer.getResource() + + def getID(self): + return self._changer.getID(self._disc) + + def cue(self): + return self._changer.cueDisc(self._discnum, 1) + + def cueTrack(self, track): + return self._changer.cueDisc(self._discnum, track) + + def play(self): + return self._changer.play() + + def stop(self): + return self._changer.stop() + + # ContentDirectory calls this + def checkUpdate(self): + print 'cU' + curid = self.getID() + if self._lastid != curid: + print 'dU' + self.doUpdate() + self._lastid = curid + + def genChildren(self): + return dict(('%02d' % i, i) for i in xrange(1, self.getID()[1] + 1)) + + def createObject(self, i, arg): + return SLinkEChangerDiscTrack, i, (), { 'discobj': self, 'track': arg } + +# This is not a child of FSObject, since we will change the shelve on our own. +class SLinkEChanger(StorageSystem, slinke.CDPlayerProtocol): + def __init__(self, *args, **kwargs): + s = filelock.LockShelve(kwargs.pop('file'), flag='rw') + StorageSystem.__init__(self, *args, **kwargs) + #slinke.CDPlayerProtocol.__init__(self) # XXX - none exists!?! + self._changed = True + self._lock = None + self._poweroff = None + self._pendinglocks = [] + self._s = s + reactor.addSystemEventTrigger('after', 'shutdown', + self.aftershutdown) + sl = slinke.SLinkE() + config = s['config'] + s = SerialPort(sl, config['serialport'], reactor, + **config['serialkwargs']) + th = slinke.SLinkProtocol() + sl.openPort(config['port'], th) + th.connectCDPlayer(config['cdnum'], self) + self._audiores = LimitedAudioRes(config['audiodev'], + config['audiodefault']) + + def aftershutdown(self): + print 'in SLinkEChanger after shutdown' + self._s.close() + self._s = None + + def trylock(self, t, obj): + '''Try to lock the Changer, timeout in t seconds. Associate + lock w/ obj.''' + # XXX - figure out how background tasks can be aborted. + # (such as getting new CDDB ids) + assert obj is not None, 'cannot use None as lock object' + if self._lock is None: + if self._poweroff is not None: + self._poweroff.cancel() + self._poweroff = None + self._lock = obj + print 'tl: locked:', `self._lock` + return defer.succeed(True) + + d = defer.Deferred() + d.addErrback(self.droppendinglockobj, obj) + cl = reactor.callLater(t, d.errback, + failure.Failure(RuntimeError('timed out waiting for lock'))) + self._pendinglocks.append((d, obj, cl)) + return d + + def droppendinglockobj(self, failure, obj): + pobj = [ x for x in self._pendinglocks if x[1] is obj ][0] + self._pendinglocks.remove(pobj) + return failure + + def haslock(self, obj): + if self._lock is obj: + return True + + return False + + def unlock(self, obj): + print 'unlock:', `obj` + if self._lock is None: + print 'ul: not locked' + raise RuntimeError('unlocked when not locked') + + if obj is not self._lock: + print 'ul: wrong obj' + raise ValueError('unlocking when not locked by: %s, was locked by: %s' % (`obj`, self._lock)) + + if not self._pendinglocks: + print 'really unlocked' + self._lock = None + self._poweroff = reactor.callLater(300, self.turnoff) + return + + pobj = self._pendinglocks.pop(0) + self._lock = pobj[1] + print 'passing lock:', `self._lock` + pobj[2].cancel() + pobj[0].callback(True) + + @defer.deferredGenerator + def turnoff(self): + # needs to be first as checkids may reschedule + # XXX - This really should only be done at start up, or + # door open events. + self._poweroff = None + a = defer.waitForDeferred(self.checkids()) + yield a + a.getResult() + print 'powering cd changer off' + + # checkids may have rescheduled us. If we don't cancel it, + # we'd wake up every five minutes just to turn off again. + if self._poweroff is not None: + self._poweroff.cancel() + self._poweroff = None + a = defer.waitForDeferred(self.transport.poweroff()) + yield a + a.getResult() + + @defer.deferredGenerator + def checkids(self): + print 'starting checkids' + a = defer.waitForDeferred(self.transport.poweron()) + yield a + print 'power should be on:', `a.getResult()` + discs = list(self.transport.discs()) + discs.sort() + print discs + for i in self.transport: + discnum = i + i = str(i) + + try: + id = self.getID(i) + #print `i`, `id`, `self._s[i]` + continue + except KeyError: + pass + + print 'missing:', `i` + + # No ID, fetch it. + a = defer.waitForDeferred(self.trylock(5, self)) + yield a + a.getResult() + + try: + a = defer.waitForDeferred(self.transport.getcddbid(discnum)) + yield a + cddbid = a.getResult() + a = defer.waitForDeferred(threads.deferToThread(CDDB.query, cddbid)) + yield a + queryres = a.getResult() + print 'res:', `i`, `queryres` + self._s[i] = { 'query': queryres, + 'cddbid': cddbid, } + self._changed = True + except slinke.NoDisc: + print 'Disc not present: %d' % discnum + continue + finally: + self.unlock(self) + + def cueDisc(self, disc, track): + # int is here since we talk about discs as strings + return self.transport.cuedisc(int(disc), track) + + def play(self): + return self.transport.play() + + def stop(self): + return self.transport.stop() + + def getResource(self): + return self._audiores + + def getID(self, disc): + try: + return self._s[disc]['cddbid'] + except KeyError: + if int(disc) == self.transport.curdisc: + pass + raise + + def getMatch(self, disc, wait=False): + q = self._s[disc]['query'] + if q[0] == 200: + return q[1] + elif q[0] == 202: + return + elif q[0] in (211, 210): + # 210 multiple matches + return q[1][0] + else: + raise ValueError('what to do? %s' % `self._s[disc]`) + + def getDiscTitle(self, disc, wait=False): + m = self.getMatch(disc, wait) + if m is None: + return '%03d' % int(disc) + + t = m['title'] + try: + t = t.decode('utf8') + except UnicodeDecodeError: + t = t.decode('iso8859-1') + + if t.count('/') > 1: + print 'tcount:', `t` + try: + return t.split('/', 1)[1] + except IndexError: + print 'ie:', `t` + return t + + # CDPlayerProtocol interface + def connectionMade(self): + super(SLinkEChanger, self).connectionMade() + print 'attached to cdplayer' + self._changed = True + + # Make sure we start out off, or if we are missing CDDB id's, + # that we get them. + self.turnoff() + + def stateChange(self): + print 'sC' + + # ContentDirectory calls this + def checkUpdate(self): + if self._changed: + self._changed = False + self.doUpdate() + + # StorageSystem interface + def genChildren(self): + return dict((self.getDiscTitle(x), + x) for x in self._s if isint(x) and self.transport and + int(x) in self.transport) + + #def genCurrent(self): + + def createObject(self, i, arg): + return SLinkEChangerDisc, i, (), { 'changer': self, 'disc': arg } + +def detectslinkemod(origpath, fobj): + # XXX - shelve adds the extension back + origpath, ext = os.path.splitext(origpath) + if ext == '.lock': + return None, None + + s = shelve.open(origpath, 'r') + if 'config' in s: + # XXX - expand detection + return SLinkEChanger, { 'file': origpath } + + return None, None + +if __name__ != '__main__': + registerklassfun(detectslinkemod, debug=False) +else: + # do create work + pass