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.

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