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.

270 lines
6.5 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006 John-Mark Gurney <gurney_j@resnet.uoregon.edu>
  3. #
  4. # $Id$
  5. #
  6. ffmpeg_path = '/Users/jgurney/src/ffmpeg/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, VideoItem, AudioItem, TextItem, ImageItem, Resource
  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', 'FSObject', 'FSItem', 'FSVideoItem',
  19. 'FSAudioItem', 'FSTextItem', 'FSImageItem', 'mimetoklass',
  20. 'FSDirectory',
  21. ]
  22. mimedict = static.loadMimeTypes()
  23. klassfuns = []
  24. def registerklassfun(fun):
  25. klassfuns.append(fun)
  26. def statcmp(a, b, cmpattrs = [ 'st_ino', 'st_dev', 'st_size', 'st_mtime', ]):
  27. if a is None or b is None:
  28. return False
  29. for i in cmpattrs:
  30. if getattr(a, i) != getattr(b, i):
  31. return False
  32. return True
  33. class FSObject(object):
  34. def __init__(self, path):
  35. self.FSpath = path
  36. self.pstat = None
  37. def checkUpdate(self):
  38. # need to handle no such file or directory
  39. # push it up? but still need to handle disappearing
  40. try:
  41. nstat = os.stat(self.FSpath)
  42. if statcmp(self.pstat, nstat):
  43. return self
  44. self.pstat = nstat
  45. self.doUpdate()
  46. except OSError, x:
  47. log.msg('os.stat, OSError: %s' % x)
  48. if x.errno in (errno.ENOENT, errno.ENOTDIR, errno.EPERM, ):
  49. # We can't access it anymore, delete it
  50. self.cd.delItem(self.id)
  51. return None
  52. else:
  53. raise
  54. return self
  55. def doUpdate(self):
  56. raise NotImplementedError
  57. def __repr__(self):
  58. return '<%s.%s: path: %s>' % (self.__class__.__module__,
  59. self.__class__.__name__, self.FSpath)
  60. class NullConsumer(file, abstract.FileDescriptor):
  61. implements(interfaces.IConsumer)
  62. def __init__(self):
  63. file.__init__(self, '/dev/null', 'w')
  64. abstract.FileDescriptor.__init__(self)
  65. def write(self, data):
  66. pass
  67. class DynamTransfer(protocol.ProcessProtocol):
  68. def __init__(self, path, mods, request):
  69. self.path = path
  70. self.mods = mods
  71. self.request = request
  72. def outReceived(self, data):
  73. self.request.write(data)
  74. def outConnectionLost(self):
  75. if self.request:
  76. self.request.unregisterProducer()
  77. self.request.finish()
  78. self.request = None
  79. def errReceived(self, data):
  80. pass
  81. #log.msg(data)
  82. def stopProducing(self):
  83. if self.request:
  84. self.request.unregisterProducer()
  85. self.request.finish()
  86. if self.proc:
  87. self.proc.loseConnection()
  88. self.proc.signalProcess('INT')
  89. self.request = None
  90. self.proc = None
  91. pauseProducing = lambda x: x.proc.pauseProducing()
  92. resumeProducing = lambda x: x.proc.resumeProducing()
  93. def render(self):
  94. mods = self.mods
  95. path = self.path
  96. request = self.request
  97. vcodec = mods[0]
  98. if mods[0] not in ('xvid', 'mpeg2', ):
  99. vcodec = 'xvid'
  100. mimetype = { 'xvid': 'video/avi', 'mpeg2': 'video/mpeg', }
  101. request.setHeader('content-type', mimetype[vcodec])
  102. if request.method == 'HEAD':
  103. return ''
  104. optdict = {
  105. 'xvid': [ '-vcodec', 'xvid',
  106. #'-mv4', '-gmc', '-g', '240',
  107. '-f', 'avi', ],
  108. 'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
  109. '-f', 'mpeg', ],
  110. }
  111. audio = [ '-acodec', 'mp3', '-ab', '192', ]
  112. args = [ 'ffmpeg', '-i', path, '-b', '8000',
  113. #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6',
  114. ] + optdict[vcodec] + audio + [ '-', ]
  115. #log.msg(*args)
  116. self.proc = process.Process(reactor, ffmpeg_path, args,
  117. None, None, self)
  118. self.proc.closeStdin()
  119. request.registerProducer(self, 1)
  120. return server.NOT_DONE_YET
  121. class DynamicTrans(resource.Resource):
  122. isLeaf = True
  123. def __init__(self, path, notrans):
  124. self.path = path
  125. self.notrans = notrans
  126. def render(self, request):
  127. if request.postpath:
  128. # Translation request
  129. return DynamTransfer(self.path, request.postpath, request).render()
  130. else:
  131. return self.notrans.render(request)
  132. class FSItem(FSObject, Item):
  133. def __init__(self, *args, **kwargs):
  134. FSObject.__init__(self, kwargs['path'])
  135. del kwargs['path']
  136. mimetype = kwargs['mimetype']
  137. del kwargs['mimetype']
  138. kwargs['content'] = DynamicTrans(self.FSpath,
  139. static.File(self.FSpath, mimetype))
  140. Item.__init__(self, *args, **kwargs)
  141. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  142. self.mimetype = mimetype
  143. def doUpdate(self):
  144. self.res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
  145. self.res.size = os.path.getsize(self.FSpath)
  146. self.res = [ self.res ]
  147. self.res.append(Resource(self.url + '/mpeg2', 'http-get:*:%s:*' % 'video/mpeg'))
  148. self.res.append(Resource(self.url + '/xvid', 'http-get:*:%s:*' % 'video/avi'))
  149. Item.doUpdate(self)
  150. def defFS(path, fobj):
  151. if os.path.isdir(path):
  152. # new dir
  153. return FSDirectory, { 'path': path }
  154. elif os.path.isfile(path):
  155. # new file - fall through to below
  156. pass
  157. else:
  158. log.msg('skipping (not dir or reg): %s' % path)
  159. return None, None
  160. klass, mt = FileDIDL.buildClassMT(FSItem, path)
  161. return klass, { 'path': path, 'mimetype': mt }
  162. def dofileadd(cd, parent, path, name):
  163. klass = None
  164. fsname = os.path.join(path, name)
  165. try:
  166. fobj = open(fsname)
  167. except:
  168. fobj = None
  169. for i in itertools.chain(klassfuns, ( defFS, )):
  170. try:
  171. try:
  172. fobj.seek(0) # incase the call expects a clean file
  173. except:
  174. pass
  175. #log.msg('testing:', `i`, `fsname`, `fobj`)
  176. klass, kwargs = i(fsname, fobj)
  177. if klass is not None:
  178. break
  179. except:
  180. #import traceback
  181. #traceback.print_exc(file=log.logfile)
  182. pass
  183. if klass is None:
  184. return
  185. #log.msg('matched:', os.path.join(path, name), `i`, `klass`)
  186. return cd.addItem(parent, klass, name, **kwargs)
  187. class FSDirectory(FSObject, StorageFolder):
  188. def __init__(self, *args, **kwargs):
  189. path = kwargs['path']
  190. del kwargs['path']
  191. StorageFolder.__init__(self, *args, **kwargs)
  192. FSObject.__init__(self, path)
  193. # mapping from path to objectID
  194. self.pathObjmap = {}
  195. def doUpdate(self):
  196. # We need to rescan this dir, and see if our children has
  197. # changed any.
  198. doupdate = False
  199. children = sets.Set(os.listdir(self.FSpath))
  200. for i in self.pathObjmap.keys():
  201. if i not in children:
  202. doupdate = True
  203. # delete
  204. self.cd.delItem(self.pathObjmap[i])
  205. del self.pathObjmap[i]
  206. for i in children:
  207. if i in self.pathObjmap:
  208. continue
  209. # new object
  210. nf = dofileadd(self.cd, self.id, self.FSpath, i)
  211. if nf is not None:
  212. doupdate = True
  213. self.pathObjmap[i] = nf
  214. # sort our children
  215. self.sort(lambda x, y: cmp(x.title, y.title))
  216. # Pass up to handle UpdateID
  217. if doupdate:
  218. StorageFolder.doUpdate(self)