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.

295 lines
7.5 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>
  3. __version__ = '$Change$'
  4. # $Id$
  5. ffmpeg_path = '/a/home/jmg/src/ffmpeg/ffmpeg'
  6. ffmpeg_path = '/usr/local/bin/ffmpeg'
  7. import FileDIDL
  8. import errno
  9. import itertools
  10. import os
  11. import sets
  12. import stat
  13. from DIDLLite import StorageFolder, Item, Resource, ResourceList
  14. from twisted.web import resource, server, static
  15. from twisted.python import log
  16. from twisted.internet import abstract, interfaces, process, protocol, reactor
  17. from zope.interface import implements
  18. __all__ = [ 'registerklassfun', 'registerfiletoignore',
  19. 'FSObject', 'FSItem', 'FSDirectory',
  20. ]
  21. mimedict = static.loadMimeTypes()
  22. _klassfuns = []
  23. def registerklassfun(fun, debug=False):
  24. _klassfuns.append((fun, debug))
  25. _filestoignore = {
  26. '.DS_Store': None
  27. }
  28. def registerfiletoignore(f):
  29. _filestoignore[f] = None
  30. # Return this class when you want the file to be skipped. If you return this,
  31. # no other modules will be applied, and it won't be added. Useful for things
  32. # like .DS_Store which are known to useless on a media server.
  33. class IgnoreFile:
  34. pass
  35. def statcmp(a, b, cmpattrs = [ 'st_ino', 'st_dev', 'st_size', 'st_mtime', ]):
  36. if a is None or b is None:
  37. return False
  38. for i in cmpattrs:
  39. if getattr(a, i) != getattr(b, i):
  40. return False
  41. return True
  42. class FSObject(object):
  43. def __init__(self, path):
  44. self.FSpath = path
  45. self.pstat = None
  46. def checkUpdate(self, **kwargs):
  47. # need to handle no such file or directory
  48. # push it up? but still need to handle disappearing
  49. try:
  50. nstat = os.stat(self.FSpath)
  51. if statcmp(self.pstat, nstat):
  52. return
  53. self.pstat = nstat
  54. self.doUpdate(**kwargs)
  55. except OSError, x:
  56. log.msg('os.stat, OSError: %s' % x)
  57. if x.errno in (errno.ENOENT, errno.ENOTDIR, errno.EPERM, ):
  58. # We can't access it anymore, delete it
  59. self.cd.delItem(self.id)
  60. return
  61. else:
  62. raise
  63. def __repr__(self):
  64. return '<%s.%s: path: %s, id: %s, parent: %s, title: %s>' % \
  65. (self.__class__.__module__, self.__class__.__name__,
  66. self.FSpath, self.id, self.parentID, self.title)
  67. class NullConsumer(file, abstract.FileDescriptor):
  68. implements(interfaces.IConsumer)
  69. def __init__(self):
  70. file.__init__(self, '/dev/null', 'w')
  71. abstract.FileDescriptor.__init__(self)
  72. def write(self, data):
  73. pass
  74. class DynamTransfer(protocol.ProcessProtocol):
  75. def __init__(self, path, mods, request):
  76. self.path = path
  77. self.mods = mods
  78. self.request = request
  79. def outReceived(self, data):
  80. self.request.write(data)
  81. def outConnectionLost(self):
  82. if self.request:
  83. self.request.unregisterProducer()
  84. self.request.finish()
  85. self.request = None
  86. def errReceived(self, data):
  87. pass
  88. #log.msg(data)
  89. def stopProducing(self):
  90. if self.request:
  91. self.request.unregisterProducer()
  92. self.request.finish()
  93. if self.proc:
  94. self.proc.loseConnection()
  95. self.proc.signalProcess('INT')
  96. self.request = None
  97. self.proc = None
  98. pauseProducing = lambda x: x.proc.pauseProducing()
  99. resumeProducing = lambda x: x.proc.resumeProducing()
  100. def render(self):
  101. mods = self.mods
  102. path = self.path
  103. request = self.request
  104. mimetype = { 'xvid': 'video/x-msvideo',
  105. 'mpeg2': 'video/mpeg',
  106. 'mp4': 'video/mp4',
  107. }
  108. vcodec = mods[0]
  109. if mods[0] not in mimetype:
  110. vcodec = 'mp4'
  111. request.setHeader('content-type', mimetype[vcodec])
  112. if request.method == 'HEAD':
  113. return ''
  114. audiomp3 = [ '-acodec', 'mp3', '-ab', '192k', '-ac', '2', ]
  115. audiomp2 = [ '-acodec', 'mp2', '-ab', '256k', '-ac', '2', ]
  116. audioac3 = [ '-acodec', 'ac3', '-ab', '640k', ]
  117. audioaac = [ '-acodec', 'aac', '-ab', '640k', ]
  118. optdict = {
  119. 'xvid': [ '-vcodec', 'xvid',
  120. #'-mv4', '-gmc', '-g', '240',
  121. '-f', 'avi', ] + audiomp3,
  122. 'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
  123. '-f', 'mpegts', ] + audioac3,
  124. 'mp4': [ '-vcodec', 'libx264', #'-g', '60',
  125. '-f', 'mpegts', ] + audioaac,
  126. }
  127. args = [ 'ffmpeg', '-i', path,
  128. '-sameq',
  129. '-threads', '4',
  130. #'-vb', '8000k',
  131. #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6',
  132. ] + optdict[vcodec] + [ '-', ]
  133. log.msg(*[`i` for i in args])
  134. self.proc = process.Process(reactor, ffmpeg_path, args,
  135. None, None, self)
  136. self.proc.closeStdin()
  137. request.registerProducer(self, 1)
  138. return server.NOT_DONE_YET
  139. class DynamicTrans(resource.Resource):
  140. isLeaf = True
  141. def __init__(self, path, notrans):
  142. self.path = path
  143. self.notrans = notrans
  144. def render(self, request):
  145. #if request.getHeader('getcontentfeatures.dlna.org'):
  146. # request.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0')
  147. # # we only want the headers
  148. # self.notrans.render(request)
  149. # request.unregisterProducer()
  150. # return ''
  151. if request.postpath:
  152. # Translation request
  153. return DynamTransfer(self.path, request.postpath, request).render()
  154. else:
  155. return self.notrans.render(request)
  156. class FSItem(FSObject, Item):
  157. def __init__(self, *args, **kwargs):
  158. FSObject.__init__(self, kwargs.pop('path'))
  159. mimetype = kwargs.pop('mimetype')
  160. kwargs['content'] = DynamicTrans(self.FSpath,
  161. static.File(self.FSpath, mimetype))
  162. Item.__init__(self, *args, **kwargs)
  163. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  164. self.mimetype = mimetype
  165. self.checkUpdate()
  166. def doUpdate(self):
  167. #print 'FSItem doUpdate:', `self`
  168. self.res = ResourceList()
  169. r = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
  170. r.size = os.path.getsize(self.FSpath)
  171. self.res.append(r)
  172. if self.mimetype.split('/', 1)[0] == 'video':
  173. self.res.append(Resource(self.url + '/mpeg2',
  174. 'http-get:*:%s:*' % 'video/mpeg'))
  175. self.res.append(Resource(self.url + '/xvid',
  176. 'http-get:*:%s:*' % 'video/x-msvideo'))
  177. Item.doUpdate(self)
  178. def ignoreFiles(path, fobj):
  179. bn = os.path.basename(path)
  180. if bn in _filestoignore:
  181. return IgnoreFile, None
  182. elif bn[:2] == '._' and open(path).read(4) == '\x00\x05\x16\x07':
  183. # AppleDouble encoded Macintosh Resource Fork
  184. return IgnoreFile, None
  185. return None, None
  186. def defFS(path, fobj):
  187. if os.path.isdir(path):
  188. # new dir
  189. return FSDirectory, { 'path': path }
  190. elif os.path.isfile(path):
  191. # new file - fall through to below
  192. pass
  193. else:
  194. log.msg('skipping (not dir or reg): %s' % path)
  195. return None, None
  196. klass, mt = FileDIDL.buildClassMT(FSItem, path)
  197. return klass, { 'path': path, 'mimetype': mt }
  198. def dofileadd(path, name):
  199. klass = None
  200. fsname = os.path.join(path, name)
  201. try:
  202. fobj = open(fsname)
  203. except:
  204. fobj = None
  205. for i, debug in itertools.chain(( (ignoreFiles, False), ), _klassfuns, ( (defFS, False), )):
  206. try:
  207. try:
  208. # incase the call expects a clean file
  209. fobj.seek(0)
  210. except:
  211. pass
  212. #log.msg('testing:', `i`, `fsname`, `fobj`)
  213. klass, kwargs = i(fsname, fobj)
  214. if klass is not None:
  215. break
  216. except:
  217. if debug:
  218. import traceback
  219. traceback.print_exc(file=log.logfile)
  220. if klass is None or klass is IgnoreFile:
  221. return None, None, None, None
  222. #print 'matched:', os.path.join(path, name), `i`, `klass`
  223. return klass, name, (), kwargs
  224. class FSDirectory(FSObject, StorageFolder):
  225. def __init__(self, *args, **kwargs):
  226. path = kwargs['path']
  227. del kwargs['path']
  228. StorageFolder.__init__(self, *args, **kwargs)
  229. FSObject.__init__(self, path)
  230. def genCurrent(self):
  231. return ((x.id, os.path.basename(x.FSpath)) for x in self )
  232. def genChildren(self):
  233. return os.listdir(self.FSpath)
  234. def createObject(self, i):
  235. return dofileadd(self.FSpath, i)
  236. def __repr__(self):
  237. return ('<%s.%s: path: %s, id: %s, parent: %s, title: %s, ' + \
  238. 'cnt: %d>') % (self.__class__.__module__,
  239. self.__class__.__name__, self.FSpath, self.id,
  240. self.parentID, self.title, len(self))