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.

221 lines
6.1 KiB

  1. #!/usr/bin/env python
  2. # Licensed under the MIT license
  3. # http://opensource.org/licenses/mit-license.php
  4. # Copyright 2005, Tim Potter <tpot@samba.org>
  5. # Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>
  6. __version__ = '$Change: 1740 $'
  7. # $Id: //depot/python/pymeds/main/pymeds.py#17 $
  8. # make sure debugging is initalized first, other modules can be pulled in
  9. # before the "real" debug stuff is setup. (hmm I could make this a two
  10. # stage, where we simulate a namespace to either be thrown away when the
  11. # time comes, or merge into the correct one)
  12. import debug # my debugging module
  13. debug.doDebugging(False) # open up debugging port
  14. # Modules to import, maybe config file or something?
  15. def tryloadmodule(mod):
  16. try:
  17. return __import__(mod)
  18. except ImportError:
  19. import traceback
  20. traceback.print_exc()
  21. pass
  22. # ZipStorage w/ tar support should be last as it will gobble up empty files.
  23. # These should be sorted by how much work they do, the least work the earlier.
  24. # mpegtsmod can be really expensive.
  25. modules = [
  26. 'audio',
  27. 'Clip',
  28. 'pyvr',
  29. 'audioraw',
  30. 'item',
  31. 'slinkemod',
  32. 'dvd',
  33. 'ZipStorage',
  34. 'mpegtsmod',
  35. ]
  36. modmap = {}
  37. for i in modules:
  38. modmap[i] = tryloadmodule(i)
  39. for i in modules:
  40. debug.insertnamespace(i, modmap[i])
  41. # Check to see which ones didn't get loaded
  42. checkmodules = [ x for x in modmap if modmap[x] is None ]
  43. if checkmodules:
  44. checkmodules.sort()
  45. print('The following modules were not loaded:', ', '.join(checkmodules))
  46. from FSStorage import FSDirectory
  47. import os
  48. import os.path
  49. import random
  50. import socket
  51. import string
  52. import urllib.parse
  53. from twisted.application import internet, service
  54. from twisted.python import usage
  55. def generateuuid():
  56. return ''.join([ 'uuid:'] + [random.choice(string.ascii_letters) for x in range(20)])
  57. class Options(usage.Options):
  58. checkpath = True
  59. optParameters = [
  60. [ 'title', 't', 'My Media Server', 'Title of the server.', ],
  61. [ 'path', 'p', 'media', 'Root path of the media to be served.', ],
  62. ]
  63. def postOptions(self):
  64. p = self['path']
  65. if self.checkpath and not os.path.isdir(p):
  66. raise usage.UsageError('path %s does not exist' % repr(p))
  67. def parseArgs(self, *args):
  68. # XXX - twisted doesn't let you provide a message on what
  69. # arguments are required, so we will do our own work in here.
  70. if len(args) not in (1, 2):
  71. raise usage.UsageError('Arguments: addr [ port ]')
  72. self['addr'] = args[0]
  73. if len(args) == 1:
  74. port = random.randint(10000, 65000)
  75. else:
  76. port = int(args[1])
  77. if port < 1024 or port > 65535:
  78. raise ValueError(
  79. 'port must be between 1024 and 65535')
  80. self['port'] = port
  81. def fixupmimetypes():
  82. # Purely to ensure some sane mime-types. On MacOSX I need these.
  83. # XXX - There isn't any easier way to get to the mime-type dict
  84. # that I know of.
  85. from twisted.web import static
  86. medianode = static.File('pymediaserv')
  87. medianode.contentTypes.update( {
  88. # From: http://support.microsoft.com/kb/288102
  89. '.asf': 'video/x-ms-asf',
  90. '.asx': 'video/x-ms-asf',
  91. '.wma': 'audio/x-ms-wma',
  92. '.wax': 'audio/x-ms-wax',
  93. '.wmv': 'video/x-ms-wmv',
  94. '.wvx': 'video/x-ms-wvx',
  95. '.wm': 'video/x-ms-wm',
  96. '.wmx': 'video/x-ms-wmx',
  97. # From: http://www.matroska.org/technical/specs/notes.html
  98. '.mkv': 'video/x-matroska',
  99. '.mka': 'audio/x-matroska',
  100. '.flv': 'video/x-flv',
  101. #'.ts': 'video/mp2t',
  102. '.ts': 'video/mpeg', # we may want this instead of mp2t
  103. '.m2t': 'video/mpeg',
  104. '.m2ts': 'video/mpeg',
  105. '.mp4': 'video/mp4',
  106. #'.mp4': 'video/mpeg',
  107. '.dat': 'video/mpeg', # VCD tracks
  108. '.ogm': 'application/ogg',
  109. '.vob': 'video/mpeg',
  110. #'.m4a': 'audio/mp4', # D-Link can't seem to play AAC files.
  111. })
  112. def makeService(config):
  113. listenAddr = config['addr']
  114. listenPort = config['port']
  115. uuid = config.get('uuid', None)
  116. if uuid is None:
  117. uuid = generateuuid()
  118. urlbase = 'http://%s:%d/' % (listenAddr, listenPort)
  119. # Create SOAP server and content server
  120. from twisted.web import server, resource, static
  121. from ContentDirectory import ContentDirectoryServer
  122. from ConnectionManager import ConnectionManagerServer
  123. class WebServer(resource.Resource):
  124. def __init__(self):
  125. resource.Resource.__init__(self)
  126. class RootDevice(static.Data):
  127. def __init__(self):
  128. r = {
  129. 'hostname': socket.gethostname(),
  130. 'uuid': uuid,
  131. 'urlbase': urlbase,
  132. }
  133. d = open('root-device.xml').read() % r
  134. static.Data.__init__(self, bytes(d, 'ascii'), 'text/xml')
  135. root = WebServer()
  136. debug.insertnamespace('root', root)
  137. content = resource.Resource()
  138. # This sets up the root to be the media dir so we don't have to
  139. # enumerate the directory.
  140. cds = ContentDirectoryServer(config['title'], klass=FSDirectory,
  141. path=config['path'], urlbase=urllib.parse.urljoin(urlbase, 'content'),
  142. webbase=content)
  143. debug.insertnamespace('cds', cds)
  144. root.putChild(b'ContentDirectory', cds)
  145. cds = cds.control
  146. root.putChild(b'ConnectionManager', ConnectionManagerServer())
  147. root.putChild(b'root-device.xml', RootDevice())
  148. root.putChild(b'content', content)
  149. fixupmimetypes()
  150. site = server.Site(root)
  151. # Create SSDP server
  152. from SSDP import SSDPServer, SSDP_PORT
  153. s = SSDPServer()
  154. debug.insertnamespace('s', s)
  155. class PyMedS(service.MultiService):
  156. def startService(self):
  157. service.MultiService.startService(self)
  158. rdxml = urllib.parse.urljoin(urlbase, 'root-device.xml')
  159. s.register('%s::upnp:rootdevice' % uuid,
  160. 'upnp:rootdevice', rdxml)
  161. s.register(uuid,
  162. uuid,
  163. rdxml)
  164. s.register('%s::urn:schemas-upnp-org:device:MediaServer:1' % uuid,
  165. 'urn:schemas-upnp-org:device:MediaServer:1', rdxml)
  166. s.register('%s::urn:schemas-upnp-org:service:ConnectionManager:1' % uuid,
  167. 'urn:schemas-upnp-org:service:ConnectionManager:1', rdxml)
  168. s.register('%s::urn:schemas-upnp-org:service:ContentDirectory:1' % uuid,
  169. 'urn:schemas-upnp-org:service:ContentDirectory:1', rdxml)
  170. def stopService(self):
  171. # Some reason stopProtocol isn't called
  172. s.doStop()
  173. service.MultiService.stopService(self)
  174. import pickle
  175. pickle.dump(cds, open('test.pickle', 'wb'), -1)
  176. serv = PyMedS()
  177. internet.TCPServer(listenPort, site, interface=listenAddr).setServiceParent(serv)
  178. internet.MulticastServer(SSDP_PORT, s,
  179. listenMultiple=True).setServiceParent(serv)
  180. return serv