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.

178 lines
4.8 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 John-Mark Gurney <jmg@funkthat.com>
  6. __version__ = '$Change$'
  7. # $Id$
  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(True) # 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. 'shoutcast',
  27. 'pyvr',
  28. 'dvd',
  29. 'ZipStorage',
  30. 'mpegtsmod',
  31. ]
  32. modmap = {}
  33. for i in modules:
  34. modmap[i] = tryloadmodule(i)
  35. for i in modules:
  36. debug.insertnamespace(i, modmap[i])
  37. from DIDLLite import TextItem, AudioItem, VideoItem, ImageItem, Resource, StorageFolder
  38. from FSStorage import FSDirectory
  39. import os
  40. import os.path
  41. import random
  42. import socket
  43. import string
  44. import sys
  45. from twisted.python import log
  46. from twisted.internet import reactor
  47. def generateuuid():
  48. if False:
  49. return 'uuid:asdflkjewoifjslkdfj'
  50. return ''.join([ 'uuid:'] + map(lambda x: random.choice(string.letters), xrange(20)))
  51. listenAddr = sys.argv[1]
  52. if len(sys.argv) > 2:
  53. listenPort = int(sys.argv[2])
  54. if listenPort < 1024 or listenPort > 65535:
  55. raise ValueError, 'port out of range'
  56. else:
  57. listenPort = random.randint(10000, 65000)
  58. log.startLogging(sys.stdout)
  59. # Create SSDP server
  60. from SSDP import SSDPServer, SSDP_PORT, SSDP_ADDR
  61. s = SSDPServer()
  62. debug.insertnamespace('s', s)
  63. port = reactor.listenMulticast(SSDP_PORT, s, listenMultiple=True)
  64. port.joinGroup(SSDP_ADDR)
  65. port.setLoopbackMode(0) # don't get our own sends
  66. uuid = generateuuid()
  67. urlbase = 'http://%s:%d/' % (listenAddr, listenPort)
  68. # Create SOAP server
  69. from twisted.web import server, resource, static
  70. from ContentDirectory import ContentDirectoryServer
  71. from ConnectionManager import ConnectionManagerServer
  72. class WebServer(resource.Resource):
  73. def __init__(self):
  74. resource.Resource.__init__(self)
  75. class RootDevice(static.Data):
  76. def __init__(self):
  77. r = {
  78. 'hostname': socket.gethostname(),
  79. 'uuid': uuid,
  80. 'urlbase': urlbase,
  81. }
  82. d = file('root-device.xml').read() % r
  83. static.Data.__init__(self, d, 'text/xml')
  84. root = WebServer()
  85. debug.insertnamespace('root', root)
  86. content = resource.Resource()
  87. mediapath = 'media'
  88. if not os.path.isdir(mediapath):
  89. print >>sys.stderr, \
  90. 'Sorry, %s is not a directory, no content to serve.' % `mediapath`
  91. sys.exit(1)
  92. # This sets up the root to be the media dir so we don't have to
  93. # enumerate the directory
  94. cds = ContentDirectoryServer('My Media Server', klass=FSDirectory,
  95. path=mediapath, urlbase=os.path.join(urlbase, 'content'), webbase=content)
  96. debug.insertnamespace('cds', cds)
  97. root.putChild('ContentDirectory', cds)
  98. cds = cds.control
  99. root.putChild('ConnectionManager', ConnectionManagerServer())
  100. root.putChild('root-device.xml', RootDevice())
  101. root.putChild('content', content)
  102. # Purely to ensure some sane mime-types. On MacOSX I need these.
  103. medianode = static.File('pymediaserv')
  104. medianode.contentTypes.update( {
  105. # From: http://support.microsoft.com/kb/288102
  106. '.asf': 'video/x-ms-asf',
  107. '.asx': 'video/x-ms-asf',
  108. '.wma': 'audio/x-ms-wma',
  109. '.wax': 'audio/x-ms-wax',
  110. '.wmv': 'video/x-ms-wmv',
  111. '.wvx': 'video/x-ms-wvx',
  112. '.wm': 'video/x-ms-wm',
  113. '.wmx': 'video/x-ms-wmx',
  114. #'.ts': 'video/mp2t',
  115. '.ts': 'video/mpeg', # we may want this instead of mp2t
  116. '.m2t': 'video/mpeg',
  117. '.m2ts': 'video/mpeg',
  118. '.mp4': 'video/mp4',
  119. #'.mp4': 'video/mpeg',
  120. '.dat': 'video/mpeg', # VCD tracks
  121. '.ogm': 'application/ogg',
  122. '.vob': 'video/mpeg',
  123. #'.m4a': 'audio/mp4', # D-Link can't seem to play AAC files.
  124. })
  125. del medianode
  126. site = server.Site(root)
  127. reactor.listenTCP(listenPort, site)
  128. # we need to do this after the children are there, since we send notifies
  129. s.register('%s::upnp:rootdevice' % uuid,
  130. 'upnp:rootdevice',
  131. urlbase + 'root-device.xml')
  132. s.register(uuid,
  133. uuid,
  134. urlbase + 'root-device.xml')
  135. s.register('%s::urn:schemas-upnp-org:device:MediaServer:1' % uuid,
  136. 'urn:schemas-upnp-org:device:MediaServer:1',
  137. urlbase + 'root-device.xml')
  138. s.register('%s::urn:schemas-upnp-org:service:ConnectionManager:1' % uuid,
  139. 'urn:schemas-upnp-org:device:ConnectionManager:1',
  140. urlbase + 'root-device.xml')
  141. s.register('%s::urn:schemas-upnp-org:service:ContentDirectory:1' % uuid,
  142. 'urn:schemas-upnp-org:device:ContentDirectory:1',
  143. urlbase + 'root-device.xml')
  144. # Main loop
  145. reactor.run()