Browse Source

preliminary version of the audio and slink modules... should

include a sample asrc file..

[git-p4: depot-paths = "//depot/": change = 1380]
main
John-Mark Gurney 15 years ago
parent
commit
ed038868f9
6 changed files with 1969 additions and 0 deletions
  1. +4
    -0
      README
  2. +275
    -0
      audio.py
  3. +44
    -0
      cddb.py
  4. +2
    -0
      pymeds.py
  5. +1085
    -0
      slinke.py
  6. +559
    -0
      slinkemod.py

+ 4
- 0
README View File

@@ -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:


+ 275
- 0
audio.py View File

@@ -0,0 +1,275 @@
#!/usr/bin/env python
# Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
'''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 '<AudioPlayer: fileno: %d, connected: %s, self.disconnecting: %s, _writeDisconnected: %s>' % (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()

+ 44
- 0
cddb.py View File

@@ -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:]))

+ 2
- 0
pymeds.py View File

@@ -29,9 +29,11 @@ def tryloadmodule(mod):
# mpegtsmod can be really expensive.
modules = [
'shoutcast',
'audio',
'Clip',
'pyvr',
'item',
'slinkemod',
'dvd',
'ZipStorage',
'mpegtsmod',


+ 1085
- 0
slinke.py
File diff suppressed because it is too large
View File


+ 559
- 0
slinkemod.py View File

@@ -0,0 +1,559 @@
#!/usr/bin/env python
# Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
'''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

Loading…
Cancel
Save