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.

340 lines
10 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. reqname = 'requests'
  23. from twisted.python import log
  24. from twisted.web import resource, static
  25. from elementtree.ElementTree import Element, SubElement, tostring
  26. from upnp import UPnPPublisher, errorCode
  27. from DIDLLite import DIDLElement, Container, Movie, Resource, MusicTrack
  28. from twisted.internet import defer
  29. from twisted.python import failure
  30. import debug
  31. import traceback
  32. from urllib import quote
  33. class doRecall(defer.Deferred):
  34. '''A class that will upon any callback from the Deferred object passed
  35. in, recall fun(*args, **kwargs), just as if a maybeDeferred has been
  36. processed.
  37. The idea is to let something deeper called by something sync "abort"
  38. the call until it's ready, and then reattempt. This isn't the best
  39. method as we throw away work, but it can be easier to implement.
  40. Example:
  41. def wrapper(five):
  42. try:
  43. return doacall(five)
  44. except defer.Deferred, x:
  45. return doRecallgen(x, wrapper, five)
  46. If doacall works, everything is fine, but if a Deferred object is
  47. raised, we put it in a doRecall class and return the deferred object
  48. generated by doRecall.'''
  49. def __init__(self, argdef, fun, *args, **kwargs):
  50. self.fun = fun
  51. self.args = args
  52. self.kwargs = kwargs
  53. self.defer = defer.Deferred()
  54. argdef.addCallback(self._done)
  55. def _done(self, *args, **kwargs):
  56. ret = self.fun(*self.args, **self.kwargs)
  57. if isinstance(ret, failure.Failure):
  58. self.defer.errback(ret)
  59. elif isinstance(ret, defer.Deferred):
  60. # We are fruther delayed, continue.
  61. ret.addCallback(self._done)
  62. else:
  63. self.defer.callback(ret)
  64. def doRecallgen(defer, fun, *args, **kwargs):
  65. i = doRecall(defer, fun, *args, **kwargs)
  66. return i.defer
  67. class ContentDirectoryControl(UPnPPublisher, dict):
  68. """This class implements the CDS actions over SOAP."""
  69. urlbase = property(lambda x: x._urlbase)
  70. def getnextID(self):
  71. ret = str(self.nextID)
  72. self.nextID += 1
  73. return ret
  74. def addContainer(self, parent, title, klass = Container, *args, **kwargs):
  75. ret = self.addObject(parent, klass, title, *args, **kwargs)
  76. self.children[ret] = self[ret]
  77. return ret
  78. def addItem(self, parent, klass, title, *args, **kwargs):
  79. if issubclass(klass, Container):
  80. return self.addContainer(parent, title, klass, *args, **kwargs)
  81. else:
  82. return self.addObject(parent, klass, title, *args, **kwargs)
  83. def addObject(self, parent, klass, title, *args, **kwargs):
  84. '''If the generated object (by klass) has an attribute content, it is installed into the web server.'''
  85. assert isinstance(self[parent], Container)
  86. nid = self.getnextID()
  87. i = klass(self, nid, parent, title, *args, **kwargs)
  88. if hasattr(i, 'content'):
  89. self.webbase.putChild(nid, i.content)
  90. #log.msg('children:', `self.children[parent]`, `i`)
  91. self.children[parent].append(i)
  92. self[i.id] = i
  93. return i.id
  94. def has_key(self, key):
  95. return dict.has_key(self, key)
  96. def delItem(self, id):
  97. if not self.has_key(id):
  98. log.msg('already removed:', id)
  99. return
  100. #log.msg('removing:', id)
  101. if isinstance(self[id], Container):
  102. #log.msg('children:', Container.__repr__(self.children[id]), map(None, self.children[id]))
  103. while self.children[id]:
  104. self.delItem(self.children[id][0].id)
  105. assert len(self.children[id]) == 0
  106. del self.children[id]
  107. # Remove from parent
  108. self.children[self[id].parentID].remove(self[id])
  109. # Remove content
  110. if hasattr(self[id], 'content'):
  111. self.webbase.delEntity(id)
  112. del self[id]
  113. def getchildren(self, item):
  114. assert isinstance(self[item], Container)
  115. return self.children[item][:]
  116. def __init__(self, title, *args, **kwargs):
  117. debug.insertringbuf(reqname)
  118. super(ContentDirectoryControl, self).__init__(*args)
  119. self.webbase = kwargs['webbase']
  120. self._urlbase = kwargs['urlbase']
  121. del kwargs['webbase'], kwargs['urlbase']
  122. fakeparent = '-1'
  123. self.nextID = 0
  124. self.children = { fakeparent: []}
  125. self[fakeparent] = Container(None, None, '-1', 'fake')
  126. root = self.addContainer(fakeparent, title, **kwargs)
  127. assert root == '0'
  128. del self[fakeparent]
  129. del self.children[fakeparent]
  130. # Required actions
  131. def soap_GetSearchCapabilities(self, *args, **kwargs):
  132. """Required: Return the searching capabilities supported by the device."""
  133. log.msg('GetSearchCapabilities()')
  134. return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }}
  135. def soap_GetSortCapabilities(self, *args, **kwargs):
  136. """Required: Return the CSV list of meta-data tags that can be used in
  137. sortCriteria."""
  138. log.msg('GetSortCapabilities()')
  139. return { 'SortCapabilitiesResponse': { 'SortCaps': '' }}
  140. def soap_GetSystemUpdateID(self, *args, **kwargs):
  141. """Required: Return the current value of state variable SystemUpdateID."""
  142. log.msg('GetSystemUpdateID()')
  143. return { 'SystemUpdateIdResponse': { 'Id': self['0'].updateID }}
  144. BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren')
  145. def soap_Browse(self, *args):
  146. l = {}
  147. debug.appendnamespace(reqname, l)
  148. l['query'] = 'Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' \
  149. 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % \
  150. tuple(map(repr, args))
  151. try:
  152. ret = self.thereal_soap_Browse(*args)
  153. except defer.Deferred, x:
  154. ret = doRecallgen(x, self.soap_Browse, *args)
  155. l['response'] = `ret`
  156. return ret
  157. def thereal_soap_Browse(self, *args):
  158. """Required: Incrementally browse the native heirachy of the Content
  159. Directory objects exposed by the Content Directory Service."""
  160. (ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount,
  161. SortCriteria) = args
  162. StartingIndex = int(StartingIndex)
  163. RequestedCount = int(RequestedCount)
  164. didl = DIDLElement()
  165. result = {}
  166. # check to see if object needs to be updated
  167. self[ObjectID].checkUpdate()
  168. # return error code if we don't exist
  169. if ObjectID not in self:
  170. raise errorCode(701)
  171. if BrowseFlag == 'BrowseDirectChildren':
  172. ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount]
  173. # filter out the ones that don't exist anymore, we need
  174. # to check against None, since some dirs might be empty
  175. # (of valid content) but exist.
  176. # XXX - technically if list changed, we need to get
  177. # some new ones by looping till we have a complete
  178. # list.
  179. ochup = filter(lambda x, s = self: s.has_key(x.id) and
  180. s[x.id].checkUpdate() is not None, ch)
  181. if len(ochup) != len(ch):
  182. log.msg('ch:', `ch`, 'ochup:', `ochup`)
  183. raise RuntimeError, 'something disappeared'
  184. filter(lambda x, d = didl: d.addItem(x) and None, ochup)
  185. total = len(self.getchildren(ObjectID))
  186. else:
  187. didl.addItem(self[ObjectID])
  188. total = 1
  189. result = {'BrowseResponse': {'Result': didl.toString() ,
  190. 'NumberReturned': didl.numItems(),
  191. 'TotalMatches': total,
  192. 'UpdateID': self[ObjectID].updateID }}
  193. #log.msg('Returning: %s' % result)
  194. return result
  195. # Optional actions
  196. def soap_Search(self, *args, **kwargs):
  197. """Search for objects that match some search criteria."""
  198. (ContainerID, SearchCriteria, Filter, StartingIndex,
  199. RequestedCount, SortCriteria) = args
  200. log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \
  201. 'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' %
  202. (`ContainerID`, `SearchCriteria`, `Filter`,
  203. `StartingIndex`, `RequestedCount`, `SortCriteria`))
  204. def soap_CreateObject(self, *args, **kwargs):
  205. """Create a new object."""
  206. (ContainerID, Elements) = args
  207. log.msg('CreateObject(ContainerID=%s, Elements=%s)' %
  208. (`ContainerID`, `Elements`))
  209. def soap_DestroyObject(self, *args, **kwargs):
  210. """Destroy the specified object."""
  211. (ObjectID) = args
  212. log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`)
  213. def soap_UpdateObject(self, *args, **kwargs):
  214. """Modify, delete or insert object metadata."""
  215. (ObjectID, CurrentTagValue, NewTagValue) = args
  216. log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \
  217. 'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`,
  218. `NewTagValue`))
  219. def soap_ImportResource(self, *args, **kwargs):
  220. """Transfer a file from a remote source to a local
  221. destination in the Content Directory Service."""
  222. (SourceURI, DestinationURI) = args
  223. log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' %
  224. (`SourceURI`, `DestinationURI`))
  225. def soap_ExportResource(self, *args, **kwargs):
  226. """Transfer a file from a local source to a remote
  227. destination."""
  228. (SourceURI, DestinationURI) = args
  229. log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' %
  230. (`SourceURI`, `DestinationURI`))
  231. def soap_StopTransferResource(self, *args, **kwargs):
  232. """Stop a file transfer initiated by ImportResource or
  233. ExportResource."""
  234. (TransferID) = args
  235. log.msg('StopTransferResource(TransferID=%s)' % TransferID)
  236. def soap_GetTransferProgress(self, *args, **kwargs):
  237. """Query the progress of a file transfer initiated by
  238. an ImportResource or ExportResource action."""
  239. (TransferID, TransferStatus, TransferLength, TransferTotal) = args
  240. log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \
  241. 'TransferLength=%s, TransferTotal=%s)' %
  242. (`TransferId`, `TransferStatus`, `TransferLength`,
  243. `TransferTotal`))
  244. def soap_DeleteResource(self, *args, **kwargs):
  245. """Delete a specified resource."""
  246. (ResourceURI) = args
  247. log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`)
  248. def soap_CreateReference(self, *args, **kwargs):
  249. """Create a reference to an existing object."""
  250. (ContainerID, ObjectID) = args
  251. log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' %
  252. (`ContainerID`, `ObjectID`))
  253. def __repr__(self):
  254. return '<ContentDirectoryControl: cnt: %d, urlbase: %s, nextID: %d>' % (len(self), `self.urlbase`, self.nextID)
  255. class ContentDirectoryServer(resource.Resource):
  256. def __init__(self, title, *args, **kwargs):
  257. resource.Resource.__init__(self)
  258. self.putChild('scpd.xml', static.File('content-directory-scpd.xml'))
  259. self.control = ContentDirectoryControl(title, *args, **kwargs)
  260. self.putChild('control', self.control)