# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter # Copyright 2006-2007 John-Mark Gurney __version__ = '$Change$' # $Id$ # # This module implements the Content Directory Service (CDS) service # type as documented in the ContentDirectory:1 Service Template # Version 1.01 # # # TODO: Figure out a nicer pattern for debugging soap server calls as # twisted swallows the tracebacks. At the moment I'm going: # # try: # .... # except: # traceback.print_exc(file = log.logfile) # reqname = 'requests' from twisted.python import log from twisted.web import resource, static from elementtree.ElementTree import Element, SubElement, tostring from upnp import UPnPPublisher, errorCode from DIDLLite import DIDLElement, Container, Movie, Resource, MusicTrack from twisted.internet import defer from twisted.python import failure import debug import traceback from urllib import quote class doRecall(defer.Deferred): '''A class that will upon any callback from the Deferred object passed in, recall fun(*args, **kwargs), just as if a maybeDeferred has been processed. The idea is to let something deeper called by something sync "abort" the call until it's ready, and then reattempt. This isn't the best method as we throw away work, but it can be easier to implement. Example: def wrapper(five): try: return doacall(five) except defer.Deferred, x: return doRecallgen(x, wrapper, five) If doacall works, everything is fine, but if a Deferred object is raised, we put it in a doRecall class and return the deferred object generated by doRecall.''' def __init__(self, argdef, fun, *args, **kwargs): self.fun = fun self.args = args self.kwargs = kwargs self.defer = defer.Deferred() argdef.addCallback(self._done) def _done(self, *args, **kwargs): ret = self.fun(*self.args, **self.kwargs) if isinstance(ret, failure.Failure): self.defer.errback(ret) elif isinstance(ret, defer.Deferred): # We are fruther delayed, continue. ret.addCallback(self._done) else: self.defer.callback(ret) def doRecallgen(defer, fun, *args, **kwargs): i = doRecall(defer, fun, *args, **kwargs) return i.defer class ContentDirectoryControl(UPnPPublisher, dict): """This class implements the CDS actions over SOAP.""" namespace = 'urn:schemas-upnp-org:service:ContentDirectory:1' updateID = property(lambda x: x['0'].updateID) urlbase = property(lambda x: x._urlbase) def getnextID(self): ret = str(self.nextID) self.nextID += 1 return ret def addContainer(self, parent, title, klass = Container, *args, **kwargs): ret = self.addObject(parent, klass, title, *args, **kwargs) self.children[ret] = self[ret] return ret def addItem(self, parent, klass, title, *args, **kwargs): if issubclass(klass, Container): return self.addContainer(parent, title, klass, *args, **kwargs) else: return self.addObject(parent, klass, title, *args, **kwargs) def addObject(self, parent, klass, title, *args, **kwargs): '''If the generated object (by klass) has an attribute content, it is installed into the web server.''' assert isinstance(self[parent], Container) nid = self.getnextID() i = klass(self, nid, parent, title, *args, **kwargs) if hasattr(i, 'content'): self.webbase.putChild(nid, i.content) #log.msg('children:', `self.children[parent]`, `i`) self.children[parent].append(i) self[i.id] = i return i.id def has_key(self, key): return dict.has_key(self, key) def delItem(self, id): if not self.has_key(id): log.msg('already removed:', id) return #log.msg('removing:', id) if isinstance(self[id], Container): #log.msg('children:', Container.__repr__(self.children[id]), map(None, self.children[id])) while self.children[id]: self.delItem(self.children[id][0].id) assert len(self.children[id]) == 0 del self.children[id] # Remove from parent self.children[self[id].parentID].remove(self[id]) # Remove content if hasattr(self[id], 'content'): self.webbase.delEntity(id) del self[id] def getchildren(self, item): assert isinstance(self[item], Container) return self.children[item][:] def __init__(self, title, *args, **kwargs): debug.insertringbuf(reqname) super(ContentDirectoryControl, self).__init__(*args) self.webbase = kwargs['webbase'] self._urlbase = kwargs['urlbase'] del kwargs['webbase'], kwargs['urlbase'] fakeparent = '-1' self.nextID = 0 self.children = { fakeparent: []} self[fakeparent] = Container(None, None, '-1', 'fake') root = self.addContainer(fakeparent, title, **kwargs) assert root == '0' del self[fakeparent] del self.children[fakeparent] # Required actions def soap_GetSearchCapabilities(self, *args, **kwargs): """Required: Return the searching capabilities supported by the device.""" log.msg('GetSearchCapabilities()') return { 'SearchCaps': '' } def soap_GetSortCapabilities(self, *args, **kwargs): """Required: Return the CSV list of meta-data tags that can be used in sortCriteria.""" log.msg('GetSortCapabilities()') return { 'SortCaps': '' } def soap_GetSystemUpdateID(self, *args, **kwargs): """Required: Return the current value of state variable SystemUpdateID.""" log.msg('GetSystemUpdateID()') return { 'Id': self.updateID } BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') def soap_Browse(self, *args): l = {} debug.appendnamespace(reqname, l) if self.has_key(args[0]): l['object'] = self[args[0]] l['query'] = 'Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' \ 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % \ tuple(map(repr, args)) try: ret = self.thereal_soap_Browse(*args) except defer.Deferred, x: ret = doRecallgen(x, self.soap_Browse, *args) l['response'] = ret return ret def thereal_soap_Browse(self, *args): """Required: Incrementally browse the native heirachy of the Content Directory objects exposed by the Content Directory Service.""" (ObjectID, BrowseFlag, Filter, StartingIndex, RequestedCount, SortCriteria) = args StartingIndex = int(StartingIndex) RequestedCount = int(RequestedCount) didl = DIDLElement() # return error code if we don't exist anymore if ObjectID not in self: raise errorCode(701) # check to see if object needs to be updated self[ObjectID].checkUpdate() # make sure we still exist, we could of deleted ourself if ObjectID not in self: raise errorCode(701) if BrowseFlag == 'BrowseDirectChildren': ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount] for i in ch: if i.needupdate: i.checkUpdate() didl.addItem(i) total = len(self.getchildren(ObjectID)) else: didl.addItem(self[ObjectID]) total = 1 r = { 'Result': didl.toString(), 'TotalMatches': total, 'NumberReturned': didl.numItems(), } if hasattr(self[ObjectID], 'updateID'): r['UpdateID'] = self[ObjectID].updateID else: r['UpdateID'] = self.updateID return r # Optional actions def soap_Search(self, *args, **kwargs): """Search for objects that match some search criteria.""" (ContainerID, SearchCriteria, Filter, StartingIndex, RequestedCount, SortCriteria) = args log.msg('Search(ContainerID=%s, SearchCriteria=%s, Filter=%s, ' \ 'StartingIndex=%s, RequestedCount=%s, SortCriteria=%s)' % (`ContainerID`, `SearchCriteria`, `Filter`, `StartingIndex`, `RequestedCount`, `SortCriteria`)) def soap_CreateObject(self, *args, **kwargs): """Create a new object.""" (ContainerID, Elements) = args log.msg('CreateObject(ContainerID=%s, Elements=%s)' % (`ContainerID`, `Elements`)) def soap_DestroyObject(self, *args, **kwargs): """Destroy the specified object.""" (ObjectID) = args log.msg('DestroyObject(ObjectID=%s)' % `ObjectID`) def soap_UpdateObject(self, *args, **kwargs): """Modify, delete or insert object metadata.""" (ObjectID, CurrentTagValue, NewTagValue) = args log.msg('UpdateObject(ObjectID=%s, CurrentTagValue=%s, ' \ 'NewTagValue=%s)' % (`ObjectID`, `CurrentTagValue`, `NewTagValue`)) def soap_ImportResource(self, *args, **kwargs): """Transfer a file from a remote source to a local destination in the Content Directory Service.""" (SourceURI, DestinationURI) = args log.msg('ImportResource(SourceURI=%s, DestinationURI=%s)' % (`SourceURI`, `DestinationURI`)) def soap_ExportResource(self, *args, **kwargs): """Transfer a file from a local source to a remote destination.""" (SourceURI, DestinationURI) = args log.msg('ExportResource(SourceURI=%s, DestinationURI=%s)' % (`SourceURI`, `DestinationURI`)) def soap_StopTransferResource(self, *args, **kwargs): """Stop a file transfer initiated by ImportResource or ExportResource.""" (TransferID) = args log.msg('StopTransferResource(TransferID=%s)' % TransferID) def soap_GetTransferProgress(self, *args, **kwargs): """Query the progress of a file transfer initiated by an ImportResource or ExportResource action.""" (TransferID, TransferStatus, TransferLength, TransferTotal) = args log.msg('GetTransferProgress(TransferID=%s, TransferStatus=%s, ' \ 'TransferLength=%s, TransferTotal=%s)' % (`TransferId`, `TransferStatus`, `TransferLength`, `TransferTotal`)) def soap_DeleteResource(self, *args, **kwargs): """Delete a specified resource.""" (ResourceURI) = args log.msg('DeleteResource(ResourceURI=%s)' % `ResourceURI`) def soap_CreateReference(self, *args, **kwargs): """Create a reference to an existing object.""" (ContainerID, ObjectID) = args log.msg('CreateReference(ContainerID=%s, ObjectID=%s)' % (`ContainerID`, `ObjectID`)) def __repr__(self): return '' % (len(self), `self.urlbase`, self.nextID) class ContentDirectoryServer(resource.Resource): def __init__(self, title, *args, **kwargs): resource.Resource.__init__(self) self.putChild('scpd.xml', static.File('content-directory-scpd.xml')) self.control = ContentDirectoryControl(title, *args, **kwargs) self.putChild('control', self.control)