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