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.

284 lines
7.2 KiB

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