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.

269 lines
8.3 KiB

  1. # Licensed under the MIT license
  2. # http://opensource.org/licenses/mit-license.php
  3. # Copyright 2005, Tim Potter <tpot@samba.org>
  4. # Copyright 2006 John-Mark Gurney <gurney_j@resnet.uoregon.edu>
  5. #
  6. # $Id$
  7. #
  8. #
  9. # This module implements the Content Directory Service (CDS) service
  10. # type as documented in the ContentDirectory:1 Service Template
  11. # Version 1.01
  12. #
  13. #
  14. # TODO: Figure out a nicer pattern for debugging soap server calls as
  15. # twisted swallows the tracebacks. At the moment I'm going:
  16. #
  17. # try:
  18. # ....
  19. # except:
  20. # traceback.print_exc(file = log.logfile)
  21. #
  22. from twisted.python import log
  23. from twisted.web import resource, static
  24. from elementtree.ElementTree import Element, SubElement, tostring
  25. from upnp import UPnPPublisher, errorCode
  26. from DIDLLite import DIDLElement, Container, Movie, Resource, MusicTrack
  27. import traceback
  28. from urllib import quote
  29. class ContentDirectoryControl(UPnPPublisher, dict):
  30. """This class implements the CDS actions over SOAP."""
  31. urlbase = property(lambda x: x._urlbase)
  32. def getnextID(self):
  33. ret = str(self.nextID)
  34. self.nextID += 1
  35. return ret
  36. def addContainer(self, parent, title, klass = Container, *args, **kwargs):
  37. ret = self.addObject(parent, klass, title, *args, **kwargs)
  38. self.children[ret] = self[ret]
  39. return ret
  40. def addItem(self, parent, klass, title, *args, **kwargs):
  41. if issubclass(klass, Container):
  42. return self.addContainer(parent, title, klass, *args, **kwargs)
  43. else:
  44. return self.addObject(parent, klass, title, *args, **kwargs)
  45. def addObject(self, parent, klass, title, *args, **kwargs):
  46. '''If the generated object (by klass) has an attribute content, it is installed into the web server.'''
  47. assert isinstance(self[parent], Container)
  48. nid = self.getnextID()
  49. i = klass(self, nid, parent, title, *args, **kwargs)
  50. if hasattr(i, 'content'):
  51. self.webbase.putChild(nid, i.content)
  52. self.children[parent].append(i)
  53. self[i.id] = i
  54. return i.id
  55. def delItem(self, id):
  56. if not self.has_key(id):
  57. log.msg('already removed:', id)
  58. return
  59. log.msg('removing:', id)
  60. if isinstance(self[id], Container):
  61. while self.children[id]:
  62. self.delItem(self.children[id][0])
  63. assert len(self.children[id]) == 0
  64. del self.children[id]
  65. # Remove from parent
  66. self.children[self[id].parentID].remove(self[id])
  67. # Remove content
  68. if hasattr(self[id], 'content'):
  69. self.webbase.delEntity(id)
  70. del self[id]
  71. def getchildren(self, item):
  72. assert isinstance(self[item], Container)
  73. return self.children[item][:]
  74. def __init__(self, title, *args, **kwargs):
  75. super(ContentDirectoryControl, self).__init__(*args)
  76. self.webbase = kwargs['webbase']
  77. self._urlbase = kwargs['urlbase']
  78. del kwargs['webbase'], kwargs['urlbase']
  79. fakeparent = '-1'
  80. self.nextID = 0
  81. self.children = { fakeparent: []}
  82. self[fakeparent] = Container(None, None, '-1', 'fake')
  83. root = self.addContainer(fakeparent, title, **kwargs)
  84. assert root == '0'
  85. del self[fakeparent]
  86. del self.children[fakeparent]
  87. # Required actions
  88. def soap_GetSearchCapabilities(self, *args, **kwargs):
  89. """Required: Return the searching capabilities supported by the device."""
  90. log.msg('GetSearchCapabilities()')
  91. return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }}
  92. def soap_GetSortCapabilities(self, *args, **kwargs):
  93. """Required: Return the CSV list of meta-data tags that can be used in
  94. sortCriteria."""
  95. log.msg('GetSortCapabilities()')
  96. return { 'SortCapabilitiesResponse': { 'SortCaps': '' }}
  97. def soap_GetSystemUpdateID(self, *args, **kwargs):
  98. """Required: Return the current value of state variable SystemUpdateID."""
  99. log.msg('GetSystemUpdateID()')
  100. return { 'SystemUpdateIdResponse': { 'Id': self['0'].updateID }}
  101. BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren')
  102. def soap_Browse(self, *args):
  103. """Required: Incrementally browse the native heirachy of the Content
  104. Directory objects exposed by the Content Directory Service."""
  105. (ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount,
  106. SortCriteria) = args
  107. StartingIndex = int(StartingIndex)
  108. RequestedCount = int(RequestedCount)
  109. log.msg('Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, '
  110. 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' %
  111. (`ObjectID`, `BrowseFlag`, `Filter`, `StartingIndex`,
  112. `RequestedCount`, `SortCriteria`))
  113. didl = DIDLElement()
  114. result = {}
  115. # check to see if object needs to be updated
  116. self[ObjectID].checkUpdate()
  117. # return error code if we don't exist
  118. if ObjectID not in self:
  119. raise errorCode(701)
  120. if BrowseFlag == 'BrowseDirectChildren':
  121. ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount]
  122. # filter out the ones that don't exist anymore, we need
  123. # to check against None, since some dirs might be empty
  124. # (of valid content) but exist.
  125. ochup = filter(lambda x, s = self:
  126. s[x.id].checkUpdate() is not None, ch)
  127. filter(lambda x, d = didl: d.addItem(x) and None, ochup)
  128. total = len(self.getchildren(ObjectID))
  129. else:
  130. didl.addItem(self[ObjectID])
  131. total = 1
  132. result = {'BrowseResponse': {'Result': didl.toString() ,
  133. 'NumberReturned': didl.numItems(),
  134. 'TotalMatches': total,
  135. 'UpdateID': self[ObjectID].updateID }}
  136. #log.msg('Returning: %s' % result)
  137. return result
  138. # Optional actions
  139. def soap_Search(self, *args, **kwargs):
  140. """Search for objects that match some search criteria."""
  141. (ContainerID, SearchCriteria, Filter, StartingIndex,
  142. RequestedCount, SortCriteria) = args
  143. log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \
  144. 'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' %
  145. (`ContainerID`, `SearchCriteria`, `Filter`,
  146. `StartingIndex`, `RequestedCount`, `SortCriteria`))
  147. def soap_CreateObject(self, *args, **kwargs):
  148. """Create a new object."""
  149. (ContainerID, Elements) = args
  150. log.msg('CreateObject(ContainerID=%s, Elements=%s)' %
  151. (`ContainerID`, `Elements`))
  152. def soap_DestroyObject(self, *args, **kwargs):
  153. """Destroy the specified object."""
  154. (ObjectID) = args
  155. log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`)
  156. def soap_UpdateObject(self, *args, **kwargs):
  157. """Modify, delete or insert object metadata."""
  158. (ObjectID, CurrentTagValue, NewTagValue) = args
  159. log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \
  160. 'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`,
  161. `NewTagValue`))
  162. def soap_ImportResource(self, *args, **kwargs):
  163. """Transfer a file from a remote source to a local
  164. destination in the Content Directory Service."""
  165. (SourceURI, DestinationURI) = args
  166. log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' %
  167. (`SourceURI`, `DestinationURI`))
  168. def soap_ExportResource(self, *args, **kwargs):
  169. """Transfer a file from a local source to a remote
  170. destination."""
  171. (SourceURI, DestinationURI) = args
  172. log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' %
  173. (`SourceURI`, `DestinationURI`))
  174. def soap_StopTransferResource(self, *args, **kwargs):
  175. """Stop a file transfer initiated by ImportResource or
  176. ExportResource."""
  177. (TransferID) = args
  178. log.msg('StopTransferResource(TransferID=%s)' % TransferID)
  179. def soap_GetTransferProgress(self, *args, **kwargs):
  180. """Query the progress of a file transfer initiated by
  181. an ImportResource or ExportResource action."""
  182. (TransferID, TransferStatus, TransferLength, TransferTotal) = args
  183. log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \
  184. 'TransferLength=%s, TransferTotal=%s)' %
  185. (`TransferId`, `TransferStatus`, `TransferLength`,
  186. `TransferTotal`))
  187. def soap_DeleteResource(self, *args, **kwargs):
  188. """Delete a specified resource."""
  189. (ResourceURI) = args
  190. log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`)
  191. def soap_CreateReference(self, *args, **kwargs):
  192. """Create a reference to an existing object."""
  193. (ContainerID, ObjectID) = args
  194. log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' %
  195. (`ContainerID`, `ObjectID`))
  196. def __repr__(self):
  197. return '<ContentDirectoryControl: cnt: %d, urlbase: %s, nextID: %d>' % (len(self), `self.urlbase`, self.nextID)
  198. class ContentDirectoryServer(resource.Resource):
  199. def __init__(self, title, *args, **kwargs):
  200. resource.Resource.__init__(self)
  201. self.putChild('scpd.xml', static.File('content-directory-scpd.xml'))
  202. self.control = ContentDirectoryControl(title, *args, **kwargs)
  203. self.putChild('control', self.control)