# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter # Copyright 2006 John-Mark Gurney # # $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) # 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 import traceback from urllib import quote class ContentDirectoryControl(UPnPPublisher, dict): """This class implements the CDS actions over SOAP.""" 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) self.children[parent].append(i) self[i.id] = i return i.id 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): while self.children[id]: self.delItem(self.children[id][0]) 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): 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 { 'SearchCapabilitiesResponse': { '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 { 'SortCapabilitiesResponse': { 'SortCaps': '' }} def soap_GetSystemUpdateID(self, *args, **kwargs): """Required: Return the current value of state variable SystemUpdateID.""" log.msg('GetSystemUpdateID()') return { 'SystemUpdateIdResponse': { 'Id': self['0'].updateID }} BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') def 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) log.msg('Browse(ObjectID=%s, BrowseFlags=%s, Filter=%s, ' 'StartingIndex=%s RequestedCount=%s SortCriteria=%s)' % (`ObjectID`, `BrowseFlag`, `Filter`, `StartingIndex`, `RequestedCount`, `SortCriteria`)) didl = DIDLElement() result = {} # check to see if object needs to be updated self[ObjectID].checkUpdate() # return error code if we don't exist if ObjectID not in self: raise errorCode(701) if BrowseFlag == 'BrowseDirectChildren': ch = self.getchildren(ObjectID)[StartingIndex: StartingIndex + RequestedCount] # filter out the ones that don't exist anymore, we need # to check against None, since some dirs might be empty # (of valid content) but exist. ochup = filter(lambda x, s = self: s[x.id].checkUpdate() is not None, ch) filter(lambda x, d = didl: d.addItem(x) and None, ochup) total = len(self.getchildren(ObjectID)) else: didl.addItem(self[ObjectID]) total = 1 result = {'BrowseResponse': {'Result': didl.toString() , 'NumberReturned': didl.numItems(), 'TotalMatches': total, 'UpdateID': self[ObjectID].updateID }} #log.msg('Returning: %s' % result) return result # 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)