A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
10 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
  3. '''Audio Raw Converter'''
  4. import cdrtoc
  5. from DIDLLite import MusicTrack, AudioItem, MusicAlbum, Resource, ResourceList
  6. from FSStorage import FSObject, registerklassfun
  7. from twisted.web import resource, server, static
  8. from twisted.internet.interfaces import IPullProducer
  9. from zope.interface import implements
  10. decoders = {}
  11. try:
  12. import flac
  13. decoders['flac'] = flac.FLACDec
  14. except ImportError:
  15. pass
  16. mtformat = {
  17. 16: 'audio/l16', # BE signed
  18. # 8: 'audio/l8', unsigned
  19. }
  20. def makeaudiomt(bitsps, rate, nchan):
  21. try:
  22. mt = mtformat[bitsps]
  23. except KeyError:
  24. raise KeyError('No mime-type for audio format: %s.' %
  25. `bitsps`)
  26. return '%s;rate=%d;channels=%d' % (mt, rate, nchan)
  27. def makemtfromdec(dec):
  28. return makeaudiomt(dec.bitspersample, dec.samplerate,
  29. dec.channels)
  30. class DecoderProducer:
  31. implements(IPullProducer)
  32. def __init__(self, consumer, decoder, tbytes, skipbytes):
  33. '''skipbytes should always be small. It is here in case
  34. someone requests the middle of a sample.'''
  35. self.decoder = decoder
  36. self.consumer = consumer
  37. self.tbytes = tbytes
  38. self.skipbytes = skipbytes
  39. #print 'DPregP', `self`, `self.tbytes`, `self.skipbytes`
  40. consumer.registerProducer(self, False)
  41. self.resumeProducing()
  42. def __repr__(self):
  43. return '<DecoderProducer: decoder: %s, bytes left: %d, skip: %d>' % (`self.decoder`, self.tbytes, self.skipbytes)
  44. def pauseProducing(self):
  45. # XXX - bug in Twisted 8.2.0 on pipelined requests this is
  46. # called: http://twistedmatrix.com/trac/ticket/3919
  47. pass
  48. def resumeProducing(self):
  49. #print 'DPrP', `self`
  50. r = self.decoder.read(256*1024)
  51. if r:
  52. #print 'DPrP:', len(r)
  53. if self.skipbytes:
  54. cnt = min(self.skipbytes, len(r))
  55. r = r[cnt:]
  56. self.skipbytes -= cnt
  57. send = min(len(r), self.tbytes)
  58. r = r[:send]
  59. self.tbytes -= len(r)
  60. self.consumer.write(r)
  61. #print 'write %d bytes, remaining %d' % (len(r), self.tbytes)
  62. if self.tbytes:
  63. return
  64. #print 'DPurP', `self`
  65. self.consumer.unregisterProducer()
  66. self.consumer.finish()
  67. def stopProducing(self):
  68. #print 'DPsP', `self`
  69. self.decoder.close()
  70. self.decoder = None
  71. self.consumer = None
  72. class AudioResource(resource.Resource):
  73. isLeaf = True
  74. def __init__(self, f, dec, start, cnt):
  75. resource.Resource.__init__(self)
  76. self.f = f
  77. self.dec = dec
  78. self.start = start
  79. self.cnt = cnt
  80. def __repr__(self):
  81. return '<AudioResource file: %s, dec: %s, start:%d, cnt: %d>' % (`self.f`, self.dec, self.start, self.cnt)
  82. def calcrange(self, rng, l):
  83. rng = rng.strip()
  84. unit, rangeset = rng.split('=')
  85. assert unit == 'bytes', `unit`
  86. start, end = rangeset.split('-')
  87. start = int(start)
  88. if end:
  89. end = int(end)
  90. else:
  91. end = l
  92. return start, end - start + 1
  93. def render(self, request):
  94. #print 'render:', `request`
  95. decoder = self.dec(self.f)
  96. request.setHeader('content-type', makemtfromdec(decoder))
  97. bytespersample = decoder.channels * decoder.bitspersample / 8
  98. tbytes = self.cnt * bytespersample
  99. #print 'tbytes:', `tbytes`, 'cnt:', `self.cnt`
  100. skipbytes = 0
  101. request.setHeader('content-length', tbytes)
  102. request.setHeader('accept-ranges', 'bytes')
  103. if request.requestHeaders.hasHeader('range'):
  104. #print 'range req:', `request.requestHeaders.getRawHeaders('range')`
  105. start, cnt = self.calcrange(
  106. request.requestHeaders.getRawHeaders('range')[0],
  107. tbytes)
  108. skipbytes = start % bytespersample
  109. #print 'going:', start / bytespersample
  110. decoder.goto(self.start + start / bytespersample)
  111. #print 'there'
  112. request.setHeader('content-length', cnt)
  113. request.setHeader('content-range', 'bytes %s-%s/%s' %
  114. (start, start + cnt - 1, tbytes))
  115. tbytes = cnt
  116. else:
  117. decoder.goto(self.start)
  118. if request.method == 'HEAD':
  119. return ''
  120. DecoderProducer(request, decoder, tbytes, skipbytes)
  121. #print 'producing render', `decoder`, `tbytes`, `skipbytes`
  122. # and make sure the connection doesn't get closed
  123. return server.NOT_DONE_YET
  124. class AudioDisc(FSObject, MusicAlbum):
  125. def __init__(self, *args, **kwargs):
  126. self.cuesheet = kwargs.pop('cuesheet')
  127. self.cdtoc = kwargs.pop('toc', {})
  128. self.kwargs = kwargs.copy()
  129. self.file = kwargs.pop('file')
  130. nchan = kwargs.pop('channels')
  131. samprate = kwargs.pop('samplerate')
  132. bitsps = kwargs.pop('bitspersample')
  133. samples = kwargs.pop('samples')
  134. tags = kwargs.pop('tags', {})
  135. picts = kwargs.pop('pictures', {})
  136. totalbytes = nchan * samples * bitsps / 8
  137. FSObject.__init__(self, kwargs.pop('path'))
  138. # XXX - exclude track 1 pre-gap?
  139. kwargs['content'] = cont = resource.Resource()
  140. cont.putChild('audio', AudioResource(file,
  141. kwargs.pop('decoder'), 0, samples))
  142. #print 'doing construction'
  143. MusicAlbum.__init__(self, *args, **kwargs)
  144. #print 'adding resource'
  145. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  146. if 'cover' in picts:
  147. pict = picts['cover'][0]
  148. #print 'p:', `pict`
  149. cont.putChild('cover', static.Data(pict[7], pict[1]))
  150. self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
  151. self.id)
  152. self.res = ResourceList()
  153. if 'DYEAR' in tags:
  154. self.year = tags['DYEAR']
  155. if 'DGENRE' in tags:
  156. self.genre = tags['DGENRE']
  157. #if 'DTITLE' in tags:
  158. # self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
  159. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  160. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  161. samprate, nchan))
  162. r.size = totalbytes
  163. r.duration = float(samples) / samprate
  164. r.bitrate = nchan * samprate * bitsps / 8
  165. r.sampleFrequency = samprate
  166. r.bitsPerSample = bitsps
  167. r.nrAudioChannels = nchan
  168. self.res.append(r)
  169. #print 'completed'
  170. def sort(self, fun=lambda x, y: cmp(int(x.originalTrackNumber), int(y.originalTrackNumber))):
  171. return list.sort(self, fun)
  172. def genChildren(self):
  173. r = [ str(x['number']) for x in
  174. self.cuesheet['tracks_array'] if x['number'] not in
  175. (170, 255) ]
  176. #print 'gC:', `r`
  177. return r
  178. def findtrackidx(self, trk):
  179. for idx, i in enumerate(self.cuesheet['tracks_array']):
  180. if i['number'] == trk:
  181. return idx
  182. raise ValueError('track %d not found' % trk)
  183. @staticmethod
  184. def findindexintrack(trk, idx):
  185. for i in trk['indices_array']:
  186. if i['number'] == idx:
  187. return i
  188. raise ValueError('index %d not found in: %s' % (idx, trk))
  189. def gettrackstart(self, i):
  190. idx = self.findtrackidx(i)
  191. track = self.cuesheet['tracks_array'][idx]
  192. index = self.findindexintrack(track, 1)
  193. return track['offset'] + index['offset']
  194. def createObject(self, i, arg=None):
  195. '''This function returns the (class, name, *args, **kwargs)
  196. that will be passed to the addItem method of the
  197. ContentDirectory. arg will be passed the value of the dict
  198. keyed by i if genChildren is a dict.'''
  199. oi = i
  200. i = int(i)
  201. trkidx = self.findtrackidx(i)
  202. trkarray = self.cuesheet['tracks_array']
  203. kwargs = self.kwargs.copy()
  204. start = self.gettrackstart(i)
  205. kwargs['start'] = start
  206. kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
  207. #print 'track: %d, kwargs: %s' % (i, `kwargs`)
  208. kwargs['originalTrackNumber'] = i
  209. try:
  210. tinfo = self.cdtoc['tracks'][i]
  211. except KeyError:
  212. tinfo = {}
  213. if 'TITLE' in self.cdtoc:
  214. kwargs['album'] = self.cdtoc['TITLE']
  215. if 'TITLE' in tinfo:
  216. oi = tinfo['TITLE']
  217. if 'PERFORMER' in tinfo:
  218. kwargs['artist'] = tinfo['PERFORMER']
  219. print 'kwargs:', `kwargs`
  220. import traceback
  221. traceback.print_stack()
  222. return AudioRawTrack, oi, (), kwargs
  223. # XXX - figure out how to make custom mix-ins w/ other than AudioItem
  224. # metaclass?
  225. class AudioRawBase(FSObject):
  226. def __init__(self, *args, **kwargs):
  227. file = kwargs.pop('file')
  228. nchan = kwargs.pop('channels')
  229. samprate = kwargs.pop('samplerate')
  230. bitsps = kwargs.pop('bitspersample')
  231. samples = kwargs.pop('samples')
  232. startsamp = kwargs.pop('start', 0)
  233. tags = kwargs.pop('tags', {})
  234. picts = kwargs.pop('pictures', {})
  235. totalbytes = nchan * samples * bitsps / 8
  236. FSObject.__init__(self, kwargs.pop('path'))
  237. #print 'AudioRaw:', `startsamp`, `samples`
  238. kwargs['content'] = cont = resource.Resource()
  239. cont.putChild('audio', AudioResource(file,
  240. kwargs.pop('decoder'), startsamp, samples))
  241. self.baseObject.__init__(self, *args, **kwargs)
  242. if 'DYEAR' in tags:
  243. self.year = tags['DYEAR'][0]
  244. if 'DGENRE' in tags:
  245. self.genre = tags['DGENRE'][0]
  246. #if 'DTITLE' in tags:
  247. # self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
  248. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  249. if 'cover' in picts:
  250. pict = picts['cover'][0]
  251. cont.putChild('cover', static.Data(pict[7], pict[1]))
  252. self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
  253. self.id)
  254. self.res = ResourceList()
  255. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  256. samprate, nchan))
  257. r.size = totalbytes
  258. r.duration = float(samples) / samprate
  259. r.bitrate = nchan * samprate * bitsps / 8
  260. r.sampleFrequency = samprate
  261. r.bitsPerSample = bitsps
  262. r.nrAudioChannels = nchan
  263. self.res.append(r)
  264. def doUpdate(self):
  265. print 'dU:', `self`, self.baseObject.doUpdate
  266. print self.__class__.__bases__
  267. import traceback
  268. traceback.print_stack()
  269. self.baseObject.doUpdate(self)
  270. class AudioRaw(AudioRawBase, AudioItem):
  271. baseObject = AudioItem
  272. class AudioRawTrack(AudioRawBase, MusicTrack):
  273. baseObject = MusicTrack
  274. def detectaudioraw(origpath, fobj):
  275. for i in decoders.itervalues():
  276. try:
  277. obj = i(origpath)
  278. # XXX - don't support down sampling yet
  279. if obj.bitspersample not in (8, 16):
  280. continue
  281. args = {
  282. 'path': origpath,
  283. 'decoder': i,
  284. 'file': origpath,
  285. 'channels': obj.channels,
  286. 'samplerate': obj.samplerate,
  287. 'bitspersample': obj.bitspersample,
  288. 'samples': obj.totalsamples,
  289. 'tags': obj.tags,
  290. 'pictures': obj.pictures,
  291. }
  292. if obj.cuesheet is not None:
  293. try:
  294. args['toc'] = cdrtoc.parsetoc(
  295. obj.tags['cd.toc'][0])
  296. except KeyError:
  297. pass
  298. except:
  299. import traceback
  300. print 'WARNING: failed to parse toc:'
  301. traceback.print_exc()
  302. args['cuesheet'] = obj.cuesheet
  303. return AudioDisc, args
  304. return AudioRaw, args
  305. except:
  306. #import traceback
  307. #traceback.print_exc()
  308. pass
  309. return None, None
  310. registerklassfun(detectaudioraw, True)