|
- #!/usr/bin/env python
- # Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
- '''CD Changer Module'''
-
- __version__ = '$Change$'
- # $Id$
-
- 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', repr(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:', repr(request.postpath), repr(request.method), repr(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:', repr(args))
- exc.printTraceback()
- #exc.printDetailedTraceback()
-
- def failed(self, exc, request):
- print('in this failed case', self._obj.haslock(request), repr(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:', repr(self), repr(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 range(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:', repr(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:', repr(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' % (repr(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:', repr(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:', repr(a.getResult()))
- discs = sorted(self.transport.discs())
- 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:', repr(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:', repr(i), repr(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' % repr(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:', repr(t))
- try:
- return t.split('/', 1)[1]
- except IndexError:
- print('ie:', repr(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
|