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