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.

396 lines
11 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. # XXX - need to convert to cdtoc?
  130. self.tags = tags = kwargs.pop('tags', {})
  131. self.file = kwargs.pop('file')
  132. nchan = kwargs.pop('channels')
  133. samprate = kwargs.pop('samplerate')
  134. bitsps = kwargs.pop('bitspersample')
  135. samples = kwargs.pop('samples')
  136. picts = kwargs.pop('pictures', {})
  137. totalbytes = nchan * samples * bitsps / 8
  138. FSObject.__init__(self, kwargs.pop('path'))
  139. # XXX - exclude track 1 pre-gap?
  140. kwargs['content'] = cont = resource.Resource()
  141. cont.putChild('audio', AudioResource(file,
  142. kwargs.pop('decoder'), 0, samples))
  143. #print 'doing construction'
  144. MusicAlbum.__init__(self, *args, **kwargs)
  145. #print 'adding resource'
  146. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  147. if 'cover' in picts:
  148. pict = picts['cover'][0]
  149. #print 'p:', `pict`
  150. cont.putChild('cover', static.Data(pict[7], pict[1]))
  151. self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
  152. self.id)
  153. self.res = ResourceList()
  154. if 'DYEAR' in tags:
  155. self.year = tags['DYEAR']
  156. if 'DGENRE' in tags:
  157. self.genre = tags['DGENRE']
  158. #if 'DTITLE' in tags:
  159. # self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
  160. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  161. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  162. samprate, nchan))
  163. r.size = totalbytes
  164. r.duration = float(samples) / samprate
  165. r.bitrate = nchan * samprate * bitsps / 8
  166. r.sampleFrequency = samprate
  167. r.bitsPerSample = bitsps
  168. r.nrAudioChannels = nchan
  169. self.res.append(r)
  170. #print 'completed'
  171. def sort(self, fun=lambda x, y: cmp(int(x.originalTrackNumber), int(y.originalTrackNumber))):
  172. return list.sort(self, fun)
  173. def genChildren(self):
  174. r = [ str(x['number']) for x in
  175. self.cuesheet['tracks_array'] if x['number'] not in
  176. (170, 255) ]
  177. #print 'gC:', `r`
  178. return r
  179. def findtrackidx(self, trk):
  180. for idx, i in enumerate(self.cuesheet['tracks_array']):
  181. if i['number'] == trk:
  182. return idx
  183. raise ValueError('track %d not found' % trk)
  184. @staticmethod
  185. def findindexintrack(trk, idx):
  186. for i in trk['indices_array']:
  187. if i['number'] == idx:
  188. return i
  189. raise ValueError('index %d not found in: %s' % (idx, trk))
  190. def gettrackstart(self, i):
  191. idx = self.findtrackidx(i)
  192. track = self.cuesheet['tracks_array'][idx]
  193. index = self.findindexintrack(track, 1)
  194. return track['offset'] + index['offset']
  195. def createObject(self, i, arg=None):
  196. '''This function returns the (class, name, *args, **kwargs)
  197. that will be passed to the addItem method of the
  198. ContentDirectory. arg will be passed the value of the dict
  199. keyed by i if genChildren is a dict.'''
  200. oi = i
  201. i = int(i)
  202. trkidx = self.findtrackidx(i)
  203. trkarray = self.cuesheet['tracks_array']
  204. kwargs = self.kwargs.copy()
  205. start = self.gettrackstart(i)
  206. tags = self.tags
  207. #print 'tags:', `tags`
  208. kwargs['start'] = start
  209. kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
  210. #print 'track: %d, kwargs: %s' % (i, `kwargs`)
  211. kwargs['originalTrackNumber'] = i
  212. try:
  213. tinfo = self.cdtoc['tracks'][i]
  214. #print 'tinfo:', `tinfo`
  215. except KeyError:
  216. tinfo = {}
  217. if 'dtitle' in tags and ' / ' in tags['dtitle'][0]:
  218. kwargs['artist'], kwargs['album'] = tags['dtitle'][0].split(' / ')
  219. else:
  220. if 'TITLE' in self.cdtoc:
  221. kwargs['album'] = self.cdtoc['TITLE']
  222. if 'PERFORMER' in tinfo:
  223. kwargs['artist'] = tinfo['PERFORMER']
  224. tt = 'ttitle%d' % (i - 1)
  225. if tt in tags:
  226. if len(tags[tt]) != 1:
  227. # XXX - track this?
  228. print 'hun? ttitle:', `tags[tt]`
  229. ttitle = tags[tt][0]
  230. if ' / ' in ttitle:
  231. kwargs['artist'], oi = ttitle.split(' / ')
  232. else:
  233. oi = ttitle
  234. else:
  235. if 'TITLE' in tinfo:
  236. oi = tinfo['TITLE']
  237. #print 'title:', `oi`
  238. #print 'kwargs:', `kwargs`
  239. #print 'artist:', `kwargs['artist']`
  240. #import traceback
  241. #traceback.print_stack()
  242. return AudioRawTrack, oi, (), kwargs
  243. # XXX - figure out how to make custom mix-ins w/ other than AudioItem
  244. # metaclass?
  245. class AudioRawBase(FSObject):
  246. def __init__(self, *args, **kwargs):
  247. file = kwargs.pop('file')
  248. nchan = kwargs.pop('channels')
  249. samprate = kwargs.pop('samplerate')
  250. bitsps = kwargs.pop('bitspersample')
  251. samples = kwargs.pop('samples')
  252. startsamp = kwargs.pop('start', 0)
  253. tags = kwargs.pop('tags', {})
  254. picts = kwargs.pop('pictures', {})
  255. totalbytes = nchan * samples * bitsps / 8
  256. FSObject.__init__(self, kwargs.pop('path'))
  257. #print 'AudioRaw:', `startsamp`, `samples`
  258. kwargs['content'] = cont = resource.Resource()
  259. cont.putChild('audio', AudioResource(file,
  260. kwargs.pop('decoder'), startsamp, samples))
  261. self.baseObject.__init__(self, *args, **kwargs)
  262. if 'DYEAR' in tags:
  263. self.year = tags['DYEAR'][0]
  264. if 'DGENRE' in tags:
  265. self.genre = tags['DGENRE'][0]
  266. if 'DTITLE' in tags and ' / ' in tags['DTITLE'][0]:
  267. self.artist, self.album = tags['DTITLE'][0].split(' / ', 1)
  268. self.url = '%s/%s/audio' % (self.cd.urlbase, self.id)
  269. if 'cover' in picts:
  270. pict = picts['cover'][0]
  271. cont.putChild('cover', static.Data(pict[7], pict[1]))
  272. self.albumArtURI = '%s/%s/cover' % (self.cd.urlbase,
  273. self.id)
  274. self.res = ResourceList()
  275. r = Resource(self.url, 'http-get:*:%s:*' % makeaudiomt(bitsps,
  276. samprate, nchan))
  277. r.size = totalbytes
  278. r.duration = float(samples) / samprate
  279. r.bitrate = nchan * samprate * bitsps / 8
  280. r.sampleFrequency = samprate
  281. r.bitsPerSample = bitsps
  282. r.nrAudioChannels = nchan
  283. self.res.append(r)
  284. def doUpdate(self):
  285. print 'dU:', `self`, self.baseObject.doUpdate
  286. print self.__class__.__bases__
  287. #import traceback
  288. #traceback.print_stack()
  289. self.baseObject.doUpdate(self)
  290. class AudioRaw(AudioRawBase, AudioItem):
  291. baseObject = AudioItem
  292. class AudioRawTrack(AudioRawBase, MusicTrack):
  293. baseObject = MusicTrack
  294. def detectaudioraw(origpath, fobj):
  295. for i in decoders.itervalues():
  296. try:
  297. obj = i(origpath)
  298. # XXX - don't support down sampling yet
  299. if obj.bitspersample not in (8, 16):
  300. continue
  301. args = {
  302. 'path': origpath,
  303. 'decoder': i,
  304. 'file': origpath,
  305. 'channels': obj.channels,
  306. 'samplerate': obj.samplerate,
  307. 'bitspersample': obj.bitspersample,
  308. 'samples': obj.totalsamples,
  309. 'tags': obj.tags,
  310. 'pictures': obj.pictures,
  311. }
  312. if obj.cuesheet is not None:
  313. try:
  314. args['toc'] = cdrtoc.parsetoc(
  315. obj.tags['cd.toc'][0])
  316. except KeyError:
  317. pass
  318. except:
  319. import traceback
  320. print 'WARNING: failed to parse toc:'
  321. traceback.print_exc()
  322. args['cuesheet'] = obj.cuesheet
  323. return AudioDisc, args
  324. return AudioRaw, args
  325. except:
  326. #import traceback
  327. #traceback.print_exc()
  328. pass
  329. return None, None
  330. registerklassfun(detectaudioraw, True)